Compare commits
No commits in common. "notashelf/push-rlkyvyzyzxqy" and "main" have entirely different histories.
notashelf/
...
main
17 changed files with 986 additions and 2191 deletions
592
Cargo.lock
generated
592
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
16
Cargo.toml
16
Cargo.toml
|
|
@ -1,25 +1,29 @@
|
|||
[workspace]
|
||||
members = ["cognos", "rom"]
|
||||
members = [ "cognos", "rom" ]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
name = "rom"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
authors = ["NotAShelf <raf@notashelf.dev>"]
|
||||
description = "Pretty build graphs for Nix builds"
|
||||
rust-version = "1.91.1"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/notashelf/rom"
|
||||
homepage = "https://github.com/notashelf/rom"
|
||||
rust-version = "1.85"
|
||||
readme = true
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.100"
|
||||
clap = { version = "4.5.51", features = ["derive"] }
|
||||
clap = { version = "4.5.48", features = ["derive"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.145"
|
||||
serde_repr = "0.1.20"
|
||||
crossterm = "0.29.0"
|
||||
ratatui = "0.29.0"
|
||||
indexmap = { version = "2.12.0", features = ["serde"] }
|
||||
csv = "1.4.0"
|
||||
chrono = "0.4.42"
|
||||
indexmap = { version = "2.11.4", features = ["serde"] }
|
||||
csv = "1.3.1"
|
||||
thiserror = "2.0.17"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
||||
|
|
|
|||
610
LICENSE
610
LICENSE
|
|
@ -1,288 +1,328 @@
|
|||
EUROPEAN UNION PUBLIC LICENCE v. 1.2
|
||||
EUPL © the European Union 2007, 2016
|
||||
|
||||
This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined
|
||||
below) which is provided under the terms of this Licence. Any use of the Work,
|
||||
other than as authorised under this Licence is prohibited (to the extent such
|
||||
use is covered by a right of the copyright holder of the Work).
|
||||
|
||||
The Work is provided under the terms of this Licence when the Licensor (as
|
||||
defined below) has placed the following notice immediately following the
|
||||
copyright notice for the Work:
|
||||
|
||||
Licensed under the EUPL
|
||||
|
||||
or has expressed by any other means his willingness to license under the EUPL.
|
||||
Mozilla Public License, version 2.0
|
||||
|
||||
1. Definitions
|
||||
|
||||
In this Licence, the following terms have the following meaning:
|
||||
|
||||
- ‘The Licence’: this Licence.
|
||||
|
||||
- ‘The Original Work’: the work or software distributed or communicated by the
|
||||
Licensor under this Licence, available as Source Code and also as Executable
|
||||
Code as the case may be.
|
||||
|
||||
- ‘Derivative Works’: the works or software that could be created by the
|
||||
Licensee, based upon the Original Work or modifications thereof. This Licence
|
||||
does not define the extent of modification or dependence on the Original Work
|
||||
required in order to classify a work as a Derivative Work; this extent is
|
||||
determined by copyright law applicable in the country mentioned in Article 15.
|
||||
|
||||
- ‘The Work’: the Original Work or its Derivative Works.
|
||||
|
||||
- ‘The Source Code’: the human-readable form of the Work which is the most
|
||||
convenient for people to study and modify.
|
||||
|
||||
- ‘The Executable Code’: any code which has generally been compiled and which is
|
||||
meant to be interpreted by a computer as a program.
|
||||
|
||||
- ‘The Licensor’: the natural or legal person that distributes or communicates
|
||||
the Work under the Licence.
|
||||
|
||||
- ‘Contributor(s)’: any natural or legal person who modifies the Work under the
|
||||
Licence, or otherwise contributes to the creation of a Derivative Work.
|
||||
|
||||
- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of
|
||||
the Work under the terms of the Licence.
|
||||
|
||||
- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,
|
||||
renting, distributing, communicating, transmitting, or otherwise making
|
||||
available, online or offline, copies of the Work or providing access to its
|
||||
essential functionalities at the disposal of any other natural or legal
|
||||
person.
|
||||
|
||||
2. Scope of the rights granted by the Licence
|
||||
|
||||
The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
|
||||
sublicensable licence to do the following, for the duration of copyright vested
|
||||
in the Original Work:
|
||||
|
||||
- use the Work in any circumstance and for all usage,
|
||||
- reproduce the Work,
|
||||
- modify the Work, and make Derivative Works based upon the Work,
|
||||
- communicate to the public, including the right to make available or display
|
||||
the Work or copies thereof to the public and perform publicly, as the case may
|
||||
be, the Work,
|
||||
- distribute the Work or copies thereof,
|
||||
- lend and rent the Work or copies thereof,
|
||||
- sublicense rights in the Work or copies thereof.
|
||||
|
||||
Those rights can be exercised on any media, supports and formats, whether now
|
||||
known or later invented, as far as the applicable law permits so.
|
||||
|
||||
In the countries where moral rights apply, the Licensor waives his right to
|
||||
exercise his moral right to the extent allowed by law in order to make effective
|
||||
the licence of the economic rights here above listed.
|
||||
|
||||
The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to
|
||||
any patents held by the Licensor, to the extent necessary to make use of the
|
||||
rights granted on the Work under this Licence.
|
||||
|
||||
3. Communication of the Source Code
|
||||
|
||||
The Licensor may provide the Work either in its Source Code form, or as
|
||||
Executable Code. If the Work is provided as Executable Code, the Licensor
|
||||
provides in addition a machine-readable copy of the Source Code of the Work
|
||||
along with each copy of the Work that the Licensor distributes or indicates, in
|
||||
a notice following the copyright notice attached to the Work, a repository where
|
||||
the Source Code is easily and freely accessible for as long as the Licensor
|
||||
continues to distribute or communicate the Work.
|
||||
|
||||
4. Limitations on copyright
|
||||
|
||||
Nothing in this Licence is intended to deprive the Licensee of the benefits from
|
||||
any exception or limitation to the exclusive rights of the rights owners in the
|
||||
Work, of the exhaustion of those rights or of other applicable limitations
|
||||
thereto.
|
||||
|
||||
5. Obligations of the Licensee
|
||||
|
||||
The grant of the rights mentioned above is subject to some restrictions and
|
||||
obligations imposed on the Licensee. Those obligations are the following:
|
||||
|
||||
Attribution right: The Licensee shall keep intact all copyright, patent or
|
||||
trademarks notices and all notices that refer to the Licence and to the
|
||||
disclaimer of warranties. The Licensee must include a copy of such notices and a
|
||||
copy of the Licence with every copy of the Work he/she distributes or
|
||||
communicates. The Licensee must cause any Derivative Work to carry prominent
|
||||
notices stating that the Work has been modified and the date of modification.
|
||||
|
||||
Copyleft clause: If the Licensee distributes or communicates copies of the
|
||||
Original Works or Derivative Works, this Distribution or Communication will be
|
||||
done under the terms of this Licence or of a later version of this Licence
|
||||
unless the Original Work is expressly distributed only under this version of the
|
||||
Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee
|
||||
(becoming Licensor) cannot offer or impose any additional terms or conditions on
|
||||
the Work or Derivative Work that alter or restrict the terms of the Licence.
|
||||
|
||||
Compatibility clause: If the Licensee Distributes or Communicates Derivative
|
||||
Works or copies thereof based upon both the Work and another work licensed under
|
||||
a Compatible Licence, this Distribution or Communication can be done under the
|
||||
terms of this Compatible Licence. For the sake of this clause, ‘Compatible
|
||||
Licence’ refers to the licences listed in the appendix attached to this Licence.
|
||||
Should the Licensee's obligations under the Compatible Licence conflict with
|
||||
his/her obligations under this Licence, the obligations of the Compatible
|
||||
Licence shall prevail.
|
||||
|
||||
Provision of Source Code: When distributing or communicating copies of the Work,
|
||||
the Licensee will provide a machine-readable copy of the Source Code or indicate
|
||||
a repository where this Source will be easily and freely available for as long
|
||||
as the Licensee continues to distribute or communicate the Work.
|
||||
|
||||
Legal Protection: This Licence does not grant permission to use the trade names,
|
||||
trademarks, service marks, or names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the copyright notice.
|
||||
|
||||
6. Chain of Authorship
|
||||
|
||||
The original Licensor warrants that the copyright in the Original Work granted
|
||||
hereunder is owned by him/her or licensed to him/her and that he/she has the
|
||||
power and authority to grant the Licence.
|
||||
|
||||
Each Contributor warrants that the copyright in the modifications he/she brings
|
||||
to the Work are owned by him/her or licensed to him/her and that he/she has the
|
||||
power and authority to grant the Licence.
|
||||
|
||||
Each time You accept the Licence, the original Licensor and subsequent
|
||||
Contributors grant You a licence to their contributions to the Work, under the
|
||||
terms of this Licence.
|
||||
|
||||
7. Disclaimer of Warranty
|
||||
|
||||
The Work is a work in progress, which is continuously improved by numerous
|
||||
Contributors. It is not a finished work and may therefore contain defects or
|
||||
‘bugs’ inherent to this type of development.
|
||||
|
||||
For the above reason, the Work is provided under the Licence on an ‘as is’ basis
|
||||
and without warranties of any kind concerning the Work, including without
|
||||
limitation merchantability, fitness for a particular purpose, absence of defects
|
||||
or errors, accuracy, non-infringement of intellectual property rights other than
|
||||
copyright as stated in Article 6 of this Licence.
|
||||
|
||||
This disclaimer of warranty is an essential part of the Licence and a condition
|
||||
for the grant of any rights to the Work.
|
||||
|
||||
8. Disclaimer of Liability
|
||||
|
||||
Except in the cases of wilful misconduct or damages directly caused to natural
|
||||
persons, the Licensor will in no event be liable for any direct or indirect,
|
||||
material or moral, damages of any kind, arising out of the Licence or of the use
|
||||
of the Work, including without limitation, damages for loss of goodwill, work
|
||||
stoppage, computer failure or malfunction, loss of data or any commercial
|
||||
damage, even if the Licensor has been advised of the possibility of such damage.
|
||||
However, the Licensor will be liable under statutory product liability laws as
|
||||
far such laws apply to the Work.
|
||||
|
||||
9. Additional agreements
|
||||
|
||||
While distributing the Work, You may choose to conclude an additional agreement,
|
||||
defining obligations or services consistent with this Licence. However, if
|
||||
accepting obligations, You may act only on your own behalf and on your sole
|
||||
responsibility, not on behalf of the original Licensor or any other Contributor,
|
||||
and only if You agree to indemnify, defend, and hold each Contributor harmless
|
||||
for any liability incurred by, or claims asserted against such Contributor by
|
||||
the fact You have accepted any warranty or additional liability.
|
||||
|
||||
10. Acceptance of the Licence
|
||||
|
||||
The provisions of this Licence can be accepted by clicking on an icon ‘I agree’
|
||||
placed under the bottom of a window displaying the text of this Licence or by
|
||||
affirming consent in any other similar way, in accordance with the rules of
|
||||
applicable law. Clicking on that icon indicates your clear and irrevocable
|
||||
acceptance of this Licence and all of its terms and conditions.
|
||||
|
||||
Similarly, you irrevocably accept this Licence and all of its terms and
|
||||
conditions by exercising any rights granted to You by Article 2 of this Licence,
|
||||
such as the use of the Work, the creation by You of a Derivative Work or the
|
||||
Distribution or Communication by You of the Work or copies thereof.
|
||||
|
||||
11. Information to the public
|
||||
|
||||
In case of any Distribution or Communication of the Work by means of electronic
|
||||
communication by You (for example, by offering to download the Work from a
|
||||
remote location) the distribution channel or media (for example, a website) must
|
||||
at least provide to the public the information requested by the applicable law
|
||||
regarding the Licensor, the Licence and the way it may be accessible, concluded,
|
||||
stored and reproduced by the Licensee.
|
||||
|
||||
12. Termination of the Licence
|
||||
|
||||
The Licence and the rights granted hereunder will terminate automatically upon
|
||||
any breach by the Licensee of the terms of the Licence.
|
||||
|
||||
Such a termination will not terminate the licences of any person who has
|
||||
received the Work from the Licensee under the Licence, provided such persons
|
||||
remain in full compliance with the Licence.
|
||||
|
||||
13. Miscellaneous
|
||||
|
||||
Without prejudice of Article 9 above, the Licence represents the complete
|
||||
agreement between the Parties as to the Work.
|
||||
|
||||
If any provision of the Licence is invalid or unenforceable under applicable
|
||||
law, this will not affect the validity or enforceability of the Licence as a
|
||||
whole. Such provision will be construed or reformed so as necessary to make it
|
||||
valid and enforceable.
|
||||
|
||||
The European Commission may publish other linguistic versions or new versions of
|
||||
this Licence or updated versions of the Appendix, so far this is required and
|
||||
reasonable, without reducing the scope of the rights granted by the Licence. New
|
||||
versions of the Licence will be published with a unique version number.
|
||||
|
||||
All linguistic versions of this Licence, approved by the European Commission,
|
||||
have identical value. Parties can take advantage of the linguistic version of
|
||||
their choice.
|
||||
|
||||
14. Jurisdiction
|
||||
|
||||
Without prejudice to specific agreement between parties,
|
||||
|
||||
- any litigation resulting from the interpretation of this License, arising
|
||||
between the European Union institutions, bodies, offices or agencies, as a
|
||||
Licensor, and any Licensee, will be subject to the jurisdiction of the Court
|
||||
of Justice of the European Union, as laid down in article 272 of the Treaty on
|
||||
the Functioning of the European Union,
|
||||
|
||||
- any litigation arising between other parties and resulting from the
|
||||
interpretation of this License, will be subject to the exclusive jurisdiction
|
||||
of the competent court where the Licensor resides or conducts its primary
|
||||
business.
|
||||
|
||||
15. Applicable Law
|
||||
|
||||
Without prejudice to specific agreement between parties,
|
||||
|
||||
- this Licence shall be governed by the law of the European Union Member State
|
||||
where the Licensor has his seat, resides or has his registered office,
|
||||
|
||||
- this licence shall be governed by Belgian law if the Licensor has no seat,
|
||||
residence or registered office inside a European Union Member State.
|
||||
|
||||
Appendix
|
||||
|
||||
‘Compatible Licences’ according to Article 5 EUPL are:
|
||||
|
||||
- GNU General Public License (GPL) v. 2, v. 3
|
||||
- GNU Affero General Public License (AGPL) v. 3
|
||||
- Open Software License (OSL) v. 2.1, v. 3.0
|
||||
- Eclipse Public License (EPL) v. 1.0
|
||||
- CeCILL v. 2.0, v. 2.1
|
||||
- Mozilla Public Licence (MPL) v. 2
|
||||
- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
|
||||
- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
|
||||
works other than software
|
||||
- European Union Public Licence (EUPL) v. 1.1, v. 1.2
|
||||
- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
|
||||
Reciprocity (LiLiQ-R+).
|
||||
|
||||
The European Commission may update this Appendix to later versions of the above
|
||||
licences without producing a new version of the EUPL, as long as they provide
|
||||
the rights granted in Article 2 of this Licence and protect the covered Source
|
||||
Code from exclusive appropriation.
|
||||
|
||||
All other changes or additions to this Appendix require the production of a new
|
||||
EUPL version.
|
||||
|
||||
1.1. “Contributor”
|
||||
means each individual or legal entity that creates, contributes to the
|
||||
creation of, or owns Covered Software.
|
||||
|
||||
1.2. “Contributor Version”
|
||||
means the combination of the Contributions of others (if any) used by a
|
||||
Contributor and that particular Contributor’s Contribution.
|
||||
|
||||
1.3. “Contribution”
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. “Covered Software”
|
||||
means Source Code Form to which the initial Contributor has attached the
|
||||
notice in Exhibit A, the Executable Form of such Source Code Form,
|
||||
and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. “Incompatible With Secondary Licenses”
|
||||
means
|
||||
|
||||
a. that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
b. that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the terms
|
||||
of a Secondary License.
|
||||
|
||||
1.6. “Executable Form”
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. “Larger Work”
|
||||
means a work that combines Covered Software with other material,
|
||||
in a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. “License”
|
||||
means this document.
|
||||
|
||||
1.9. “Licensable”
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently,
|
||||
any and all of the rights conveyed by this License.
|
||||
|
||||
1.10. “Modifications”
|
||||
means any of the following:
|
||||
|
||||
a. any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered Software; or
|
||||
|
||||
b. any new file in Source Code Form that contains any Covered Software.
|
||||
|
||||
1.11. “Patent Claims” of a Contributor
|
||||
means any patent claim(s), including without limitation, method, process,
|
||||
and apparatus claims, in any patent Licensable by such Contributor that
|
||||
would be infringed, but for the grant of the License, by the making,
|
||||
using, selling, offering for sale, having made, import, or transfer of
|
||||
either its Contributions or its Contributor Version.
|
||||
|
||||
1.12. “Secondary License”
|
||||
means either the GNU General Public License, Version 2.0, the
|
||||
GNU Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those licenses.
|
||||
|
||||
1.13. “Source Code Form”
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. “You” (or “Your”)
|
||||
means an individual or a legal entity exercising rights under this License.
|
||||
For legal entities, “You” includes any entity that controls,
|
||||
is controlled by, or is under common control with You. For purposes of
|
||||
this definition, “control” means (a) the power, direct or indirect,
|
||||
to cause the direction or management of such entity, whether by contract
|
||||
or otherwise, or (b) ownership of more than fifty percent (50%) of the
|
||||
outstanding shares or beneficial ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
|
||||
2.1. Grants
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
a. under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications,
|
||||
or as part of a Larger Work; and
|
||||
|
||||
b. under Patent Claims of such Contributor to make, use, sell,
|
||||
offer for sale, have made, import, and otherwise transfer either
|
||||
its Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor
|
||||
first distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
The licenses granted in this Section 2 are the only rights granted
|
||||
under this License. No additional rights or licenses will be implied
|
||||
from the distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted
|
||||
by a Contributor:
|
||||
|
||||
a. for any code that a Contributor has removed from
|
||||
Covered Software; or
|
||||
|
||||
b. for infringements caused by: (i) Your and any other third party’s
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its
|
||||
Contributor Version); or
|
||||
|
||||
c. under Patent Claims infringed by Covered Software in the
|
||||
absence of its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License
|
||||
(if permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing,
|
||||
or other equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the
|
||||
licenses granted in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
All distribution of Covered Software in Source Code Form, including
|
||||
any Modifications that You create or to which You contribute, must be
|
||||
under the terms of this License. You must inform recipients that the
|
||||
Source Code Form of the Covered Software is governed by the terms
|
||||
of this License, and how they can obtain a copy of this License.
|
||||
You may not attempt to alter or restrict the recipients’ rights
|
||||
in the Source Code Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
a. such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more than
|
||||
the cost of distribution to the recipient; and
|
||||
|
||||
b. You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients’ rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of
|
||||
Covered Software with a work governed by one or more Secondary Licenses,
|
||||
and the Covered Software is not Incompatible With Secondary Licenses,
|
||||
this License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the
|
||||
Covered Software under the terms of either this License or such
|
||||
Secondary License(s).
|
||||
|
||||
3.4. Notices
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of
|
||||
Covered Software. However, You may do so only on Your own behalf,
|
||||
and not on behalf of any Contributor. You must make it absolutely clear
|
||||
that any such warranty, support, indemnity, or liability obligation is
|
||||
offered by You alone, and You hereby agree to indemnify every Contributor
|
||||
for any liability incurred by such Contributor as a result of warranty,
|
||||
support, indemnity or liability terms You offer. You may include
|
||||
additional disclaimers of warranty and limitations of liability
|
||||
specific to any jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this License
|
||||
with respect to some or all of the Covered Software due to statute,
|
||||
judicial order, or regulation then You must: (a) comply with the terms of
|
||||
this License to the maximum extent possible; and (b) describe the limitations
|
||||
and the code they affect. Such description must be placed in a text file
|
||||
included with all distributions of the Covered Software under this License.
|
||||
Except to the extent prohibited by statute or regulation, such description
|
||||
must be sufficiently detailed for a recipient of ordinary skill
|
||||
to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means,
|
||||
this is the first time You have received notice of non-compliance with
|
||||
this License from such Contributor, and You become compliant prior to
|
||||
30 days after Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted
|
||||
to You by any and all Contributors for the Covered Software under
|
||||
Section 2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
6. Disclaimer of Warranty
|
||||
|
||||
Covered Software is provided under this License on an “as is” basis, without
|
||||
warranty of any kind, either expressed, implied, or statutory, including,
|
||||
without limitation, warranties that the Covered Software is free of defects,
|
||||
merchantable, fit for a particular purpose or non-infringing. The entire risk
|
||||
as to the quality and performance of the Covered Software is with You.
|
||||
Should any Covered Software prove defective in any respect, You
|
||||
(not any Contributor) assume the cost of any necessary servicing, repair,
|
||||
or correction. This disclaimer of warranty constitutes an essential part of
|
||||
this License. No use of any Covered Software is authorized under this
|
||||
License except under this disclaimer.
|
||||
|
||||
7. Limitation of Liability
|
||||
|
||||
Under no circumstances and under no legal theory, whether tort
|
||||
(including negligence), contract, or otherwise, shall any Contributor, or
|
||||
anyone who distributes Covered Software as permitted above, be liable to
|
||||
You for any direct, indirect, special, incidental, or consequential damages
|
||||
of any character including, without limitation, damages for lost profits,
|
||||
loss of goodwill, work stoppage, computer failure or malfunction, or any and
|
||||
all other commercial damages or losses, even if such party shall have been
|
||||
informed of the possibility of such damages. This limitation of liability
|
||||
shall not apply to liability for death or personal injury resulting from
|
||||
such party’s negligence to the extent applicable law prohibits such
|
||||
limitation. Some jurisdictions do not allow the exclusion or limitation of
|
||||
incidental or consequential damages, so this exclusion and limitation may
|
||||
not apply to You.
|
||||
|
||||
8. Litigation
|
||||
|
||||
Any litigation relating to this License may be brought only in the courts of
|
||||
a jurisdiction where the defendant maintains its principal place of business
|
||||
and such litigation shall be governed by laws of that jurisdiction, without
|
||||
reference to its conflict-of-law provisions. Nothing in this Section shall
|
||||
prevent a party’s ability to bring cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
|
||||
This License represents the complete agreement concerning the subject matter
|
||||
hereof. If any provision of this License is held to be unenforceable,
|
||||
such provision shall be reformed only to the extent necessary to make it
|
||||
enforceable. Any law or regulation which provides that the language of a
|
||||
contract shall be construed against the drafter shall not be used to construe
|
||||
this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
|
||||
10.1. New Versions
|
||||
Mozilla Foundation is the license steward. Except as provided in
|
||||
Section 10.3, no one other than the license steward has the right to
|
||||
modify or publish new versions of this License. Each version will be
|
||||
given a distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published
|
||||
by the license steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a modified
|
||||
version of this License if you rename the license and remove any
|
||||
references to the name of the license steward (except to note that such
|
||||
modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is
|
||||
Incompatible With Secondary Licenses
|
||||
If You choose to distribute Source Code Form that is
|
||||
Incompatible With Secondary Licenses under the terms of this version of
|
||||
the License, the notice described in Exhibit B of this
|
||||
License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the terms of the
|
||||
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
|
||||
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular file,
|
||||
then You may include the notice in a location (such as a LICENSE file in a
|
||||
relevant directory) where a recipient would be likely to
|
||||
look for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - “Incompatible With Secondary Licenses” Notice
|
||||
|
||||
This Source Code Form is “Incompatible With Secondary Licenses”,
|
||||
as defined by the Mozilla Public License, v. 2.0.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
[package]
|
||||
name = "cognos"
|
||||
description = "Minimalistic parser for Nix's ATerm .drv and internal-json log formats"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lib]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
pub mod aterm;
|
||||
mod internal_json;
|
||||
mod state;
|
||||
|
|
@ -11,32 +9,4 @@ pub use aterm::{
|
|||
parse_drv_file,
|
||||
};
|
||||
pub use internal_json::{Actions, Activities, Id, Verbosity};
|
||||
pub use state::{
|
||||
BuildInfo,
|
||||
BuildStatus,
|
||||
Dependencies,
|
||||
Derivation,
|
||||
Host,
|
||||
OutputName,
|
||||
ProgressState,
|
||||
State,
|
||||
};
|
||||
|
||||
/// Process a list of actions and return the resulting state
|
||||
#[must_use]
|
||||
pub fn process_actions(actions: Vec<Actions>) -> State {
|
||||
let mut state = State {
|
||||
progress: ProgressState::JustStarted,
|
||||
derivations: HashMap::new(),
|
||||
builds: HashMap::new(),
|
||||
dependencies: Dependencies {
|
||||
deps: HashMap::new(),
|
||||
},
|
||||
store_paths: HashMap::new(),
|
||||
dependency_states: HashMap::new(),
|
||||
};
|
||||
for action in actions {
|
||||
state.imbibe(action);
|
||||
}
|
||||
state
|
||||
}
|
||||
pub use state::{BuildInfo, BuildStatus, Derivation, Host, State};
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ pub enum StorePath {
|
|||
Uploaded,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum BuildStatus {
|
||||
Planned,
|
||||
Running,
|
||||
|
|
@ -19,14 +18,12 @@ pub enum BuildStatus {
|
|||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum ProgressState {
|
||||
pub enum Progress {
|
||||
JustStarted,
|
||||
InputReceived,
|
||||
Finished,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum OutputName {
|
||||
Out,
|
||||
Doc,
|
||||
|
|
@ -39,44 +36,15 @@ pub enum OutputName {
|
|||
Other(String),
|
||||
}
|
||||
|
||||
impl OutputName {
|
||||
#[must_use]
|
||||
pub fn parse(name: &str) -> Self {
|
||||
match name.to_lowercase().as_str() {
|
||||
"out" => Self::Out,
|
||||
"doc" => Self::Doc,
|
||||
"dev" => Self::Dev,
|
||||
"bin" => Self::Bin,
|
||||
"info" => Self::Info,
|
||||
"lib" => Self::Lib,
|
||||
"man" => Self::Man,
|
||||
"dist" => Self::Dist,
|
||||
_ => Self::Other(name.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Host {
|
||||
Localhost,
|
||||
Remote(String),
|
||||
}
|
||||
|
||||
impl Host {
|
||||
#[must_use]
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
Self::Localhost => "localhost",
|
||||
Self::Remote(name) => name,
|
||||
}
|
||||
}
|
||||
Local,
|
||||
Host(String),
|
||||
}
|
||||
|
||||
pub struct Derivation {
|
||||
store_path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BuildInfo {
|
||||
start: f64,
|
||||
host: Host,
|
||||
|
|
@ -92,68 +60,14 @@ pub enum DependencyState {
|
|||
}
|
||||
|
||||
pub struct Dependencies {
|
||||
pub deps: HashMap<Id, BuildInfo>,
|
||||
deps: HashMap<Id, BuildInfo>,
|
||||
}
|
||||
|
||||
// #[derive(Default)]
|
||||
pub struct State {
|
||||
pub progress: ProgressState,
|
||||
pub derivations: HashMap<Id, Derivation>,
|
||||
pub builds: HashMap<Id, BuildInfo>,
|
||||
pub dependencies: Dependencies,
|
||||
pub store_paths: HashMap<Id, StorePath>,
|
||||
pub dependency_states: HashMap<Id, DependencyState>,
|
||||
progress: Progress,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn imbibe(&mut self, action: Actions) {
|
||||
match action {
|
||||
Actions::Start {
|
||||
id,
|
||||
activity: _activity,
|
||||
..
|
||||
} => {
|
||||
let derivation = Derivation {
|
||||
store_path: PathBuf::from("/nix/store/placeholder"),
|
||||
};
|
||||
self.derivations.insert(id, derivation);
|
||||
|
||||
// Use the store_path to mark as used
|
||||
let _path = &self.derivations.get(&id).unwrap().store_path;
|
||||
|
||||
let build_info = BuildInfo {
|
||||
start: 0.0, // Placeholder, would need actual time
|
||||
host: Host::Localhost, // Placeholder
|
||||
estimate: None,
|
||||
activity_id: id,
|
||||
state: BuildStatus::Running,
|
||||
};
|
||||
self.builds.insert(id, build_info.clone());
|
||||
self.dependencies.deps.insert(id, build_info);
|
||||
|
||||
// Use the fields to mark as used
|
||||
let _start = self.builds.get(&id).unwrap().start;
|
||||
let _host = &self.builds.get(&id).unwrap().host;
|
||||
let _estimate = &self.builds.get(&id).unwrap().estimate;
|
||||
let _activity_id = self.builds.get(&id).unwrap().activity_id;
|
||||
|
||||
self.store_paths.insert(id, StorePath::Downloading);
|
||||
self.dependency_states.insert(id, DependencyState::Running);
|
||||
},
|
||||
Actions::Result { id, .. } => {
|
||||
if let Some(build) = self.builds.get_mut(&id) {
|
||||
build.state = BuildStatus::Complete;
|
||||
}
|
||||
},
|
||||
Actions::Stop { id } => {
|
||||
if let Some(build) = self.builds.get_mut(&id) {
|
||||
build.state = BuildStatus::Complete;
|
||||
}
|
||||
},
|
||||
Actions::Message { .. } => {
|
||||
// Could update progress or other state
|
||||
self.progress = ProgressState::InputReceived;
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn imbibe(&mut self, update: Actions) {}
|
||||
}
|
||||
|
|
|
|||
6
flake.lock
generated
6
flake.lock
generated
|
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1770115704,
|
||||
"narHash": "sha256-KHFT9UWOF2yRPlAnSXQJh6uVcgNcWlFqqiAZ7OVlHNc=",
|
||||
"lastModified": 1759381078,
|
||||
"narHash": "sha256-gTrEEp5gEspIcCOx9PD8kMaF1iEmfBcTbO0Jag2QhQs=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e6eae2ee2110f3d31110d5c222cd395303343b08",
|
||||
"rev": "7df7ff7d8e00218376575f0acdcc5d66741351ee",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable";
|
||||
description = "Rust Project Template";
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
[package]
|
||||
name = "rom"
|
||||
description.workspace = true
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
description.workspace = true
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
cognos = { path = "../cognos" }
|
||||
cognos = {path = "../cognos"}
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
serde.workspace = true
|
||||
|
|
@ -19,7 +19,6 @@ crossterm = "0.29"
|
|||
ratatui = "0.29"
|
||||
indexmap.workspace = true
|
||||
csv.workspace = true
|
||||
chrono.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
|
|
|||
397
rom/src/cache.rs
397
rom/src/cache.rs
|
|
@ -1,397 +0,0 @@
|
|||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{BufReader, BufWriter},
|
||||
path::PathBuf,
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use csv::{Reader, Writer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::state::BuildReport;
|
||||
|
||||
/// Maximum number of historical builds to keep per derivation
|
||||
const HISTORY_LIMIT: usize = 10;
|
||||
|
||||
/// Build report cache for CSV persistence
|
||||
pub struct BuildReportCache {
|
||||
cache_path: PathBuf,
|
||||
}
|
||||
|
||||
/// CSV row format for build reports
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct BuildReportRow {
|
||||
hostname: String,
|
||||
derivation_name: String,
|
||||
utc_time: String,
|
||||
build_seconds: u64,
|
||||
}
|
||||
|
||||
impl BuildReportCache {
|
||||
/// Create a new cache instance with the given path
|
||||
#[must_use]
|
||||
pub fn new(cache_path: PathBuf) -> Self {
|
||||
Self { cache_path }
|
||||
}
|
||||
|
||||
// FIXME: just use the dirs crate for this
|
||||
/// Get the default cache directory path
|
||||
///
|
||||
/// Uses `$XDG_STATE_HOME` if set, otherwise ``~/.local/state`
|
||||
#[must_use]
|
||||
pub fn default_cache_dir() -> PathBuf {
|
||||
if let Ok(xdg_state) = std::env::var("XDG_STATE_HOME") {
|
||||
PathBuf::from(xdg_state).join("rom")
|
||||
} else if let Ok(home) = std::env::var("HOME") {
|
||||
PathBuf::from(home).join(".local/state/rom")
|
||||
} else {
|
||||
PathBuf::from(".rom")
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default cache file path
|
||||
#[must_use]
|
||||
pub fn default_cache_path() -> PathBuf {
|
||||
Self::default_cache_dir().join("build-reports.csv")
|
||||
}
|
||||
|
||||
/// Load build reports from CSV
|
||||
///
|
||||
/// Returns empty [`HashMap`] if file doesn't exist or parsing fails
|
||||
pub fn load(&self) -> HashMap<(String, String), Vec<BuildReport>> {
|
||||
if !self.cache_path.exists() {
|
||||
return HashMap::new();
|
||||
}
|
||||
|
||||
let file = match File::open(&self.cache_path) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return HashMap::new(),
|
||||
};
|
||||
|
||||
let reader = BufReader::new(file);
|
||||
let mut csv_reader = Reader::from_reader(reader);
|
||||
|
||||
let mut reports: HashMap<(String, String), Vec<BuildReport>> =
|
||||
HashMap::new();
|
||||
|
||||
for result in csv_reader.deserialize() {
|
||||
let row: BuildReportRow = match result {
|
||||
Ok(r) => r,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let completed_at = match parse_utc_time(&row.utc_time) {
|
||||
Some(t) => t,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let report = BuildReport {
|
||||
derivation_name: row.derivation_name.clone(),
|
||||
platform: String::new(), // FIXME: not stored in CSV, for simplicity and because I'm lazy
|
||||
duration_secs: row.build_seconds as f64,
|
||||
completed_at,
|
||||
host: row.hostname.clone(),
|
||||
success: true, // only successful builds are cached
|
||||
};
|
||||
|
||||
let key = (row.hostname, row.derivation_name);
|
||||
reports.entry(key).or_default().push(report);
|
||||
}
|
||||
|
||||
// Sort each entry by timestamp (newest first) and limit to HISTORY_LIMIT
|
||||
for entries in reports.values_mut() {
|
||||
entries.sort_by(|a, b| b.completed_at.cmp(&a.completed_at));
|
||||
entries.truncate(HISTORY_LIMIT);
|
||||
}
|
||||
|
||||
reports
|
||||
}
|
||||
|
||||
/// Save build reports to CSV
|
||||
///
|
||||
/// Merges with existing reports and enforces history limit
|
||||
pub fn save(
|
||||
&self,
|
||||
reports: &HashMap<(String, String), Vec<BuildReport>>,
|
||||
) -> Result<(), std::io::Error> {
|
||||
// Ensure directory exists
|
||||
if let Some(parent) = self.cache_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
// Load existing reports to merge
|
||||
let mut merged = self.load();
|
||||
|
||||
// Merge new reports
|
||||
for ((host, drv_name), new_reports) in reports {
|
||||
let key = (host.clone(), drv_name.clone());
|
||||
let existing = merged.entry(key).or_default();
|
||||
|
||||
// Add new reports
|
||||
existing.extend(new_reports.iter().cloned());
|
||||
|
||||
// Sort by timestamp (newest first)
|
||||
existing.sort_by(|a, b| b.completed_at.cmp(&a.completed_at));
|
||||
|
||||
// Keep only most recent HISTORY_LIMIT entries
|
||||
existing.truncate(HISTORY_LIMIT);
|
||||
}
|
||||
|
||||
// Write to CSV
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&self.cache_path)?;
|
||||
|
||||
let writer = BufWriter::new(file);
|
||||
let mut csv_writer = Writer::from_writer(writer);
|
||||
|
||||
// Flatten and write all reports
|
||||
for ((hostname, derivation_name), entries) in merged {
|
||||
for report in entries {
|
||||
let row = BuildReportRow {
|
||||
hostname: hostname.clone(),
|
||||
derivation_name: derivation_name.clone(),
|
||||
utc_time: format_utc_time(report.completed_at),
|
||||
build_seconds: report.duration_secs as u64,
|
||||
};
|
||||
csv_writer.serialize(row)?;
|
||||
}
|
||||
}
|
||||
|
||||
csv_writer.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Calculate median build time from historical reports
|
||||
///
|
||||
/// Returns [`None`] if there are no reports
|
||||
#[must_use]
|
||||
pub fn calculate_median(reports: &[BuildReport]) -> Option<u64> {
|
||||
if reports.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut durations: Vec<u64> =
|
||||
reports.iter().map(|r| r.duration_secs as u64).collect();
|
||||
durations.sort_unstable();
|
||||
|
||||
let len = durations.len();
|
||||
if len % 2 == 1 {
|
||||
Some(durations[len / 2])
|
||||
} else {
|
||||
let mid1 = durations[len / 2 - 1];
|
||||
let mid2 = durations[len / 2];
|
||||
Some((mid1 + mid2) / 2)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get median build time for a specific derivation on a host
|
||||
#[must_use]
|
||||
pub fn get_estimate(
|
||||
&self,
|
||||
reports: &HashMap<(String, String), Vec<BuildReport>>,
|
||||
host: &str,
|
||||
derivation_name: &str,
|
||||
) -> Option<u64> {
|
||||
let key = (host.to_string(), derivation_name.to_string());
|
||||
let entries = reports.get(&key)?;
|
||||
Self::calculate_median(entries)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse UTC time string in format "%Y-%m-%d %H:%M:%S"
|
||||
fn parse_utc_time(s: &str) -> Option<SystemTime> {
|
||||
// Simple parsing for "YYYY-MM-DD HH:MM:SS" format
|
||||
let parts: Vec<&str> = s.split([' ', '-', ':']).collect();
|
||||
if parts.len() != 6 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let year: i64 = parts[0].parse().ok()?;
|
||||
let month: u64 = parts[1].parse().ok()?;
|
||||
let day: u64 = parts[2].parse().ok()?;
|
||||
let hour: u64 = parts[3].parse().ok()?;
|
||||
let minute: u64 = parts[4].parse().ok()?;
|
||||
let second: u64 = parts[5].parse().ok()?;
|
||||
|
||||
// Approximate conversion to Unix timestamp
|
||||
// This is a simplified calculation that doesn't handle leap years perfectly
|
||||
let days_since_epoch = (year - 1970) * 365
|
||||
+ (year - 1969) / 4
|
||||
+ days_until_month(month)
|
||||
+ day as i64
|
||||
- 1;
|
||||
let seconds_since_epoch =
|
||||
days_since_epoch as u64 * 86400 + hour * 3600 + minute * 60 + second;
|
||||
|
||||
Some(
|
||||
SystemTime::UNIX_EPOCH
|
||||
+ std::time::Duration::from_secs(seconds_since_epoch),
|
||||
)
|
||||
}
|
||||
|
||||
// FIXME: I'm really sure there's a library for this but lets just get
|
||||
// this thing compiling
|
||||
/// Calculate days until the start of a month (approximation)
|
||||
const fn days_until_month(month: u64) -> i64 {
|
||||
match month {
|
||||
1 => 0,
|
||||
2 => 31,
|
||||
3 => 59,
|
||||
4 => 90,
|
||||
5 => 120,
|
||||
6 => 151,
|
||||
7 => 181,
|
||||
8 => 212,
|
||||
9 => 243,
|
||||
10 => 273,
|
||||
11 => 304,
|
||||
12 => 334,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: does Chrono do this?
|
||||
/// Format SystemTime as UTC string in format "%Y-%m-%d %H:%M:%S"
|
||||
fn format_utc_time(time: SystemTime) -> String {
|
||||
let duration = time
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap_or_default();
|
||||
let secs = duration.as_secs();
|
||||
|
||||
let days = secs / 86400;
|
||||
let remaining = secs % 86400;
|
||||
let hours = remaining / 3600;
|
||||
let minutes = (remaining % 3600) / 60;
|
||||
let seconds = remaining % 60;
|
||||
|
||||
// Approximate conversion from days since epoch to date
|
||||
let mut year = 1970;
|
||||
let mut days_left = days as i64;
|
||||
|
||||
// Subtract full years
|
||||
while days_left >= 365 {
|
||||
if is_leap_year(year) && days_left >= 366 {
|
||||
days_left -= 366;
|
||||
year += 1;
|
||||
} else if !is_leap_year(year) {
|
||||
days_left -= 365;
|
||||
year += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate month and day
|
||||
let (month, day) = calculate_month_day(days_left as u64, is_leap_year(year));
|
||||
|
||||
format!("{year:04}-{month:02}-{day:02} {hours:02}:{minutes:02}:{seconds:02}")
|
||||
}
|
||||
|
||||
const fn is_leap_year(year: i64) -> bool {
|
||||
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
|
||||
}
|
||||
|
||||
fn calculate_month_day(days: u64, is_leap: bool) -> (u8, u8) {
|
||||
let days_in_month: [u8; 12] = if is_leap {
|
||||
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||
} else {
|
||||
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||
};
|
||||
|
||||
let mut remaining = days as i32;
|
||||
for (i, &month_days) in days_in_month.iter().enumerate() {
|
||||
if remaining < i32::from(month_days) {
|
||||
return ((i + 1) as u8, (remaining + 1) as u8);
|
||||
}
|
||||
remaining -= i32::from(month_days);
|
||||
}
|
||||
|
||||
(12, 31)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_calculate_median_odd() {
|
||||
let reports = vec![
|
||||
BuildReport {
|
||||
derivation_name: "test".to_string(),
|
||||
platform: "x86_64-linux".to_string(),
|
||||
duration_secs: 10.0,
|
||||
completed_at: SystemTime::UNIX_EPOCH,
|
||||
host: "localhost".to_string(),
|
||||
success: true,
|
||||
},
|
||||
BuildReport {
|
||||
derivation_name: "test".to_string(),
|
||||
platform: "x86_64-linux".to_string(),
|
||||
duration_secs: 20.0,
|
||||
completed_at: SystemTime::UNIX_EPOCH,
|
||||
host: "localhost".to_string(),
|
||||
success: true,
|
||||
},
|
||||
BuildReport {
|
||||
derivation_name: "test".to_string(),
|
||||
platform: "x86_64-linux".to_string(),
|
||||
duration_secs: 30.0,
|
||||
completed_at: SystemTime::UNIX_EPOCH,
|
||||
host: "localhost".to_string(),
|
||||
success: true,
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(BuildReportCache::calculate_median(&reports), Some(20));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_median_even() {
|
||||
let reports = vec![
|
||||
BuildReport {
|
||||
derivation_name: "test".to_string(),
|
||||
platform: "x86_64-linux".to_string(),
|
||||
duration_secs: 10.0,
|
||||
completed_at: SystemTime::UNIX_EPOCH,
|
||||
host: "localhost".to_string(),
|
||||
success: true,
|
||||
},
|
||||
BuildReport {
|
||||
derivation_name: "test".to_string(),
|
||||
platform: "x86_64-linux".to_string(),
|
||||
duration_secs: 20.0,
|
||||
completed_at: SystemTime::UNIX_EPOCH,
|
||||
host: "localhost".to_string(),
|
||||
success: true,
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(BuildReportCache::calculate_median(&reports), Some(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_median_empty() {
|
||||
let reports = vec![];
|
||||
assert_eq!(BuildReportCache::calculate_median(&reports), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_parse_utc_time() {
|
||||
let time =
|
||||
SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000);
|
||||
let formatted = format_utc_time(time);
|
||||
let parsed = parse_utc_time(&formatted).unwrap();
|
||||
|
||||
// Allow small difference due to approximation
|
||||
let diff = parsed
|
||||
.duration_since(time)
|
||||
.unwrap_or_else(|e| e.duration())
|
||||
.as_secs();
|
||||
assert!(diff < 86400); // less than 1 day difference
|
||||
}
|
||||
}
|
||||
185
rom/src/cli.rs
185
rom/src/cli.rs
|
|
@ -6,7 +6,6 @@ use std::{
|
|||
};
|
||||
|
||||
use clap::Parser;
|
||||
use cognos::ProgressState;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "rom", version, about = "ROM - A Nix build output monitor")]
|
||||
|
|
@ -33,14 +32,6 @@ pub struct Cli {
|
|||
/// Summary display style: concise, table, full
|
||||
#[arg(long, global = true, default_value = "concise")]
|
||||
pub summary: String,
|
||||
|
||||
/// Log prefix style: short, full, none
|
||||
#[arg(long, global = true, default_value = "short")]
|
||||
pub log_prefix: String,
|
||||
|
||||
/// Maximum number of log lines to display
|
||||
#[arg(long, global = true)]
|
||||
pub log_lines: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
|
|
@ -94,8 +85,6 @@ pub fn run() -> eyre::Result<()> {
|
|||
cli.format.clone(),
|
||||
cli.legend.clone(),
|
||||
cli.summary.clone(),
|
||||
cli.log_prefix.clone(),
|
||||
cli.log_lines,
|
||||
)?;
|
||||
Ok(())
|
||||
},
|
||||
|
|
@ -111,8 +100,6 @@ pub fn run() -> eyre::Result<()> {
|
|||
cli.format.clone(),
|
||||
cli.legend.clone(),
|
||||
cli.summary.clone(),
|
||||
cli.log_prefix.clone(),
|
||||
cli.log_lines,
|
||||
)?;
|
||||
Ok(())
|
||||
},
|
||||
|
|
@ -122,18 +109,14 @@ pub fn run() -> eyre::Result<()> {
|
|||
// If no args provided and --json is set, use piping mode from stdin
|
||||
if args.is_empty() && cli.json {
|
||||
let config = crate::types::Config {
|
||||
piping: false,
|
||||
silent: cli.silent,
|
||||
input_mode: crate::types::InputMode::Json,
|
||||
show_timers: true,
|
||||
width: None,
|
||||
format: crate::types::DisplayFormat::from_str(&cli.format),
|
||||
legend_style: cli.legend.clone(),
|
||||
summary_style: cli.summary.clone(),
|
||||
log_prefix_style: crate::types::LogPrefixStyle::from_str(
|
||||
&cli.log_prefix,
|
||||
),
|
||||
log_line_limit: cli.log_lines,
|
||||
piping: false,
|
||||
silent: cli.silent,
|
||||
input_mode: crate::types::InputMode::Json,
|
||||
show_timers: true,
|
||||
width: None,
|
||||
format: crate::types::DisplayFormat::from_str(&cli.format),
|
||||
legend_style: cli.legend.clone(),
|
||||
summary_style: cli.summary.clone(),
|
||||
};
|
||||
|
||||
let stdin = io::stdin();
|
||||
|
|
@ -156,8 +139,6 @@ pub fn run() -> eyre::Result<()> {
|
|||
cli.format.clone(),
|
||||
cli.legend.clone(),
|
||||
cli.summary.clone(),
|
||||
cli.log_prefix.clone(),
|
||||
cli.log_lines,
|
||||
)?;
|
||||
Ok(())
|
||||
},
|
||||
|
|
@ -167,18 +148,14 @@ pub fn run() -> eyre::Result<()> {
|
|||
// If no args provided and --json is set, use piping mode from stdin
|
||||
if args.is_empty() && cli.json {
|
||||
let config = crate::types::Config {
|
||||
piping: false,
|
||||
silent: cli.silent,
|
||||
input_mode: crate::types::InputMode::Json,
|
||||
show_timers: true,
|
||||
width: None,
|
||||
format: crate::types::DisplayFormat::from_str(&cli.format),
|
||||
legend_style: cli.legend.clone(),
|
||||
summary_style: cli.summary.clone(),
|
||||
log_prefix_style: crate::types::LogPrefixStyle::from_str(
|
||||
&cli.log_prefix,
|
||||
),
|
||||
log_line_limit: cli.log_lines,
|
||||
piping: false,
|
||||
silent: cli.silent,
|
||||
input_mode: crate::types::InputMode::Json,
|
||||
show_timers: true,
|
||||
width: None,
|
||||
format: crate::types::DisplayFormat::from_str(&cli.format),
|
||||
legend_style: cli.legend.clone(),
|
||||
summary_style: cli.summary.clone(),
|
||||
};
|
||||
|
||||
let stdin = io::stdin();
|
||||
|
|
@ -201,8 +178,6 @@ pub fn run() -> eyre::Result<()> {
|
|||
cli.format.clone(),
|
||||
cli.legend.clone(),
|
||||
cli.summary.clone(),
|
||||
cli.log_prefix.clone(),
|
||||
cli.log_lines,
|
||||
)?;
|
||||
Ok(())
|
||||
},
|
||||
|
|
@ -212,18 +187,14 @@ pub fn run() -> eyre::Result<()> {
|
|||
// If no args provided and --json is set, use piping mode from stdin
|
||||
if args.is_empty() && cli.json {
|
||||
let config = crate::types::Config {
|
||||
piping: false,
|
||||
silent: cli.silent,
|
||||
input_mode: crate::types::InputMode::Json,
|
||||
show_timers: true,
|
||||
width: None,
|
||||
format: crate::types::DisplayFormat::from_str(&cli.format),
|
||||
legend_style: cli.legend.clone(),
|
||||
summary_style: cli.summary.clone(),
|
||||
log_prefix_style: crate::types::LogPrefixStyle::from_str(
|
||||
&cli.log_prefix,
|
||||
),
|
||||
log_line_limit: cli.log_lines,
|
||||
piping: false,
|
||||
silent: cli.silent,
|
||||
input_mode: crate::types::InputMode::Json,
|
||||
show_timers: true,
|
||||
width: None,
|
||||
format: crate::types::DisplayFormat::from_str(&cli.format),
|
||||
legend_style: cli.legend.clone(),
|
||||
summary_style: cli.summary.clone(),
|
||||
};
|
||||
|
||||
let stdin = io::stdin();
|
||||
|
|
@ -246,8 +217,6 @@ pub fn run() -> eyre::Result<()> {
|
|||
cli.format.clone(),
|
||||
cli.legend.clone(),
|
||||
cli.summary.clone(),
|
||||
cli.log_prefix.clone(),
|
||||
cli.log_lines,
|
||||
)?;
|
||||
Ok(())
|
||||
},
|
||||
|
|
@ -269,10 +238,6 @@ pub fn run() -> eyre::Result<()> {
|
|||
format: crate::types::DisplayFormat::from_str(&cli.format),
|
||||
legend_style: cli.legend.clone(),
|
||||
summary_style: cli.summary.clone(),
|
||||
log_prefix_style: crate::types::LogPrefixStyle::from_str(
|
||||
&cli.log_prefix,
|
||||
),
|
||||
log_line_limit: cli.log_lines,
|
||||
};
|
||||
|
||||
let stdin = io::stdin();
|
||||
|
|
@ -288,7 +253,6 @@ pub fn run() -> eyre::Result<()> {
|
|||
///
|
||||
/// Everything before `--` is for the package name and rom arguments.
|
||||
/// Everything after `--` goes directly to nix.
|
||||
#[must_use]
|
||||
pub fn parse_args_with_separator(
|
||||
args: &[String],
|
||||
) -> (Vec<String>, Vec<String>) {
|
||||
|
|
@ -314,8 +278,6 @@ fn run_nix_build_wrapper(
|
|||
format: String,
|
||||
legend_style: String,
|
||||
summary_style: String,
|
||||
log_prefix: String,
|
||||
log_lines: Option<usize>,
|
||||
) -> eyre::Result<()> {
|
||||
// Validate that at least one package/flake is specified
|
||||
if package_and_rom_args.is_empty() {
|
||||
|
|
@ -346,8 +308,6 @@ fn run_nix_build_wrapper(
|
|||
format,
|
||||
legend_style,
|
||||
summary_style,
|
||||
crate::types::LogPrefixStyle::from_str(&log_prefix),
|
||||
log_lines,
|
||||
)?;
|
||||
if exit_code != 0 {
|
||||
std::process::exit(exit_code);
|
||||
|
|
@ -363,8 +323,6 @@ fn run_nix_shell_wrapper(
|
|||
format: String,
|
||||
legend_style: String,
|
||||
summary_style: String,
|
||||
log_prefix: String,
|
||||
log_lines: Option<usize>,
|
||||
) -> eyre::Result<()> {
|
||||
// Validate that at least one package/flake is specified
|
||||
if package_and_rom_args.is_empty() {
|
||||
|
|
@ -400,8 +358,6 @@ fn run_nix_shell_wrapper(
|
|||
format,
|
||||
legend_style,
|
||||
summary_style,
|
||||
crate::types::LogPrefixStyle::from_str(&log_prefix),
|
||||
log_lines,
|
||||
)?;
|
||||
|
||||
if exit_code != 0 {
|
||||
|
|
@ -433,8 +389,6 @@ fn run_nix_develop_wrapper(
|
|||
format: String,
|
||||
legend_style: String,
|
||||
summary_style: String,
|
||||
log_prefix: String,
|
||||
log_lines: Option<usize>,
|
||||
) -> eyre::Result<()> {
|
||||
// Validate that at least one package/flake is specified (can be empty for
|
||||
// current flake) develop without args is valid (uses current directory's
|
||||
|
|
@ -463,8 +417,6 @@ fn run_nix_develop_wrapper(
|
|||
format,
|
||||
legend_style,
|
||||
summary_style,
|
||||
crate::types::LogPrefixStyle::from_str(&log_prefix),
|
||||
log_lines,
|
||||
)?;
|
||||
|
||||
if exit_code != 0 {
|
||||
|
|
@ -496,8 +448,6 @@ fn run_monitored_command(
|
|||
format_str: String,
|
||||
legend_style_str: String,
|
||||
summary_style_str: String,
|
||||
log_prefix_style: crate::types::LogPrefixStyle,
|
||||
log_line_limit: Option<usize>,
|
||||
) -> eyre::Result<i32> {
|
||||
use std::{
|
||||
io::{BufRead, BufReader},
|
||||
|
|
@ -529,13 +479,6 @@ fn run_monitored_command(
|
|||
let start_time = Arc::new(Mutex::new(crate::state::current_time()));
|
||||
let start_time_clone = start_time.clone();
|
||||
|
||||
// Buffer for build logs - collected and passed to Display for coordinated
|
||||
// rendering
|
||||
let log_buffer =
|
||||
Arc::new(Mutex::new(std::collections::VecDeque::<String>::new()));
|
||||
let log_buffer_clone = log_buffer.clone();
|
||||
let log_buffer_render = log_buffer.clone();
|
||||
|
||||
// Spawn thread to read and parse stderr (where nix outputs logs)
|
||||
let stderr_thread = thread::spawn(move || {
|
||||
use tracing::debug;
|
||||
|
|
@ -550,62 +493,19 @@ fn run_monitored_command(
|
|||
if let Ok(action) = serde_json::from_str::<cognos::Actions>(json_line) {
|
||||
debug!("Parsed JSON message #{}: {:?}", json_count, action);
|
||||
|
||||
// Process the action first to update state
|
||||
// Print messages immediately to stdout
|
||||
if let cognos::Actions::Message { msg, .. } = &action {
|
||||
println!("{}", msg);
|
||||
}
|
||||
|
||||
let mut state = state_clone.lock().unwrap();
|
||||
let derivation_count_before = state.derivation_infos.len();
|
||||
crate::update::process_message(&mut state, action.clone());
|
||||
crate::update::process_message(&mut state, action);
|
||||
crate::update::maintain_state(
|
||||
&mut state,
|
||||
crate::state::current_time(),
|
||||
);
|
||||
let derivation_count_after = state.derivation_infos.len();
|
||||
|
||||
// Now handle build log messages after state is updated
|
||||
// Buffer them for coordinated rendering with the display
|
||||
match &action {
|
||||
cognos::Actions::Message { msg, .. } => {
|
||||
let mut logs = log_buffer_clone.lock().unwrap();
|
||||
logs.push_back(msg.clone());
|
||||
// Keep only recent logs based on limit
|
||||
if let Some(limit) = log_line_limit {
|
||||
while logs.len() > limit {
|
||||
logs.pop_front();
|
||||
}
|
||||
}
|
||||
},
|
||||
cognos::Actions::Result {
|
||||
fields,
|
||||
activity,
|
||||
id,
|
||||
} => {
|
||||
// Build log lines come as Result actions with FileTransfer
|
||||
// activity (101) and fields containing just the log
|
||||
// text: fields = ["log line text"]
|
||||
if matches!(activity, cognos::Activities::FileTransfer)
|
||||
&& !fields.is_empty()
|
||||
{
|
||||
if let Some(log_text) = fields[0].as_str() {
|
||||
// Get the activity prefix (e.g., "hello> ")
|
||||
let use_color = !silent;
|
||||
let prefix = state
|
||||
.get_activity_prefix(*id, &log_prefix_style, use_color)
|
||||
.unwrap_or_default();
|
||||
|
||||
let prefixed_log = format!("{prefix}{log_text}");
|
||||
let mut logs = log_buffer_clone.lock().unwrap();
|
||||
logs.push_back(prefixed_log);
|
||||
// Keep only recent logs based on limit
|
||||
if let Some(limit) = log_line_limit {
|
||||
while logs.len() > limit {
|
||||
logs.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
||||
if derivation_count_after != derivation_count_before {
|
||||
debug!(
|
||||
"Derivation count changed: {} -> {}",
|
||||
|
|
@ -616,16 +516,9 @@ fn run_monitored_command(
|
|||
debug!("Failed to parse JSON: {}", json_line);
|
||||
}
|
||||
} else {
|
||||
// Non-JSON lines, buffer them
|
||||
// Non-JSON lines, pass through
|
||||
non_json_count += 1;
|
||||
let mut logs = log_buffer_clone.lock().unwrap();
|
||||
logs.push_back(line.clone());
|
||||
// Keep only recent logs based on limit
|
||||
if let Some(limit) = log_line_limit {
|
||||
while logs.len() > limit {
|
||||
logs.pop_front();
|
||||
}
|
||||
}
|
||||
println!("{}", line);
|
||||
}
|
||||
}
|
||||
debug!(
|
||||
|
|
@ -688,16 +581,15 @@ fn run_monitored_command(
|
|||
|| !state.full_summary.planned_builds.is_empty();
|
||||
|
||||
if !silent {
|
||||
// Get buffered logs for coordinated rendering
|
||||
let logs: Vec<String> =
|
||||
log_buffer_render.lock().unwrap().iter().cloned().collect();
|
||||
|
||||
if has_activity || state.progress_state != ProgressState::JustStarted {
|
||||
if has_activity
|
||||
|| state.progress_state != crate::state::ProgressState::JustStarted
|
||||
{
|
||||
// Clear any previous timer display
|
||||
if last_timer_display.is_some() {
|
||||
display.clear_previous().ok();
|
||||
last_timer_display = None;
|
||||
}
|
||||
let _ = display.render(&state, &logs);
|
||||
let _ = display.render(&state, &[]);
|
||||
} else {
|
||||
// Show initial timer while waiting for activity
|
||||
let start = *start_time_clone.lock().unwrap();
|
||||
|
|
@ -707,7 +599,8 @@ fn run_monitored_command(
|
|||
|
||||
// Only update if changed (to avoid flicker)
|
||||
if last_timer_display.as_ref() != Some(&timer_text) {
|
||||
let _ = display.render(&state, &logs);
|
||||
display.clear_previous().ok();
|
||||
eprintln!("{}", timer_text);
|
||||
last_timer_display = Some(timer_text);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,9 @@ use crossterm::{
|
|||
use crate::state::{BuildStatus, DerivationId, State, current_time};
|
||||
|
||||
/// Format a duration in seconds to a human-readable string
|
||||
#[must_use]
|
||||
pub fn format_duration(secs: f64) -> String {
|
||||
if secs < 60.0 {
|
||||
format!("{secs:.0}s")
|
||||
format!("{:.0}s", secs)
|
||||
} else if secs < 3600.0 {
|
||||
format!("{:.0}m{:.0}s", secs / 60.0, secs % 60.0)
|
||||
} else {
|
||||
|
|
@ -64,9 +63,10 @@ impl Default for DisplayConfig {
|
|||
}
|
||||
|
||||
pub struct Display<W: Write> {
|
||||
writer: W,
|
||||
config: DisplayConfig,
|
||||
last_lines: usize,
|
||||
writer: W,
|
||||
config: DisplayConfig,
|
||||
last_lines: usize,
|
||||
using_alt_screen: bool,
|
||||
}
|
||||
|
||||
struct TreeNode {
|
||||
|
|
@ -80,6 +80,7 @@ impl<W: Write> Display<W> {
|
|||
writer,
|
||||
config,
|
||||
last_lines: 0,
|
||||
using_alt_screen: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -114,7 +115,7 @@ impl<W: Write> Display<W> {
|
|||
|
||||
let mut lines = Vec::new();
|
||||
|
||||
// Print build logs ABOVE the graph
|
||||
// Print accumulated logs first (these go above the tree)
|
||||
for log in logs {
|
||||
lines.push(log.clone());
|
||||
}
|
||||
|
|
@ -153,8 +154,6 @@ impl<W: Write> Display<W> {
|
|||
}
|
||||
|
||||
pub fn render_final(&mut self, state: &State) -> io::Result<()> {
|
||||
tracing::debug!("render_final called");
|
||||
|
||||
// Clear any previous render
|
||||
self.clear_previous()?;
|
||||
|
||||
|
|
@ -182,8 +181,6 @@ impl<W: Write> Display<W> {
|
|||
},
|
||||
}
|
||||
|
||||
tracing::debug!("render_final: {} lines to print", lines.len());
|
||||
|
||||
// Print final output (don't track last_lines since this is final)
|
||||
for line in lines {
|
||||
writeln!(self.writer, "{line}")?;
|
||||
|
|
@ -211,10 +208,8 @@ impl<W: Write> Display<W> {
|
|||
let failed = state.full_summary.failed_builds.len();
|
||||
let planned = state.full_summary.planned_builds.len();
|
||||
|
||||
let duration = current_time() - state.start_time;
|
||||
|
||||
// Always print summary (like NOM's "Finished at HH:MM:SS after Xs")
|
||||
if running > 0 || completed > 0 || failed > 0 || planned > 0 {
|
||||
let duration = current_time() - state.start_time;
|
||||
lines.push(format!(
|
||||
"{} {} {} │ {} {} │ {} {} │ {} {} │ {} {}",
|
||||
self.colored("━", Color::Blue),
|
||||
|
|
@ -229,18 +224,6 @@ impl<W: Write> Display<W> {
|
|||
self.colored("⏱", Color::Grey),
|
||||
self.format_duration(duration)
|
||||
));
|
||||
} else {
|
||||
// Nothing built - just show "Finished after Xs"
|
||||
let now = chrono::Local::now();
|
||||
let time_str = now.format("%H:%M:%S");
|
||||
lines.push(format!(
|
||||
"{} {}",
|
||||
self.colored(&format!("Finished at {time_str}"), Color::Green),
|
||||
self.colored(
|
||||
&format!("after {}", self.format_duration(duration)),
|
||||
Color::Green
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
lines
|
||||
|
|
@ -269,19 +252,19 @@ impl<W: Write> Display<W> {
|
|||
(usize, usize, usize),
|
||||
> = std::collections::HashMap::new();
|
||||
|
||||
for build in state.full_summary.running_builds.values() {
|
||||
for (_, build) in &state.full_summary.running_builds {
|
||||
let host = build.host.name().to_string();
|
||||
let entry = host_builds.entry(host).or_insert((0, 0, 0));
|
||||
entry.0 += 1;
|
||||
}
|
||||
|
||||
for build in state.full_summary.completed_builds.values() {
|
||||
for (_, build) in &state.full_summary.completed_builds {
|
||||
let host = build.host.name().to_string();
|
||||
let entry = host_builds.entry(host).or_insert((0, 0, 0));
|
||||
entry.1 += 1;
|
||||
}
|
||||
|
||||
for build in state.full_summary.failed_builds.values() {
|
||||
for (_, build) in &state.full_summary.failed_builds {
|
||||
let host = build.host.name().to_string();
|
||||
let entry = host_builds.entry(host).or_insert((0, 0, 0));
|
||||
entry.2 += 1;
|
||||
|
|
@ -293,13 +276,13 @@ impl<W: Write> Display<W> {
|
|||
(usize, usize),
|
||||
> = std::collections::HashMap::new();
|
||||
|
||||
for transfer in state.full_summary.running_downloads.values() {
|
||||
for (_, transfer) in &state.full_summary.running_downloads {
|
||||
let host = transfer.host.name().to_string();
|
||||
let entry = host_transfers.entry(host).or_insert((0, 0));
|
||||
entry.0 += 1;
|
||||
}
|
||||
|
||||
for transfer in state.full_summary.running_uploads.values() {
|
||||
for (_, transfer) in &state.full_summary.running_uploads {
|
||||
let host = transfer.host.name().to_string();
|
||||
let entry = host_transfers.entry(host).or_insert((0, 0));
|
||||
entry.1 += 1;
|
||||
|
|
@ -400,9 +383,9 @@ impl<W: Write> Display<W> {
|
|||
|| downloading > 0
|
||||
|| uploading > 0
|
||||
{
|
||||
lines.push(self.colored(&"═".repeat(60), Color::Blue).clone());
|
||||
lines.push(format!("{}", self.colored(&"═".repeat(60), Color::Blue)));
|
||||
lines.push(format!("{} Build Summary", self.colored("┃", Color::Blue)));
|
||||
lines.push(self.colored(&"─".repeat(60), Color::Blue).clone());
|
||||
lines.push(format!("{}", self.colored(&"─".repeat(60), Color::Blue)));
|
||||
|
||||
// Builds section
|
||||
if running + completed + failed > 0 {
|
||||
|
|
@ -449,7 +432,7 @@ impl<W: Write> Display<W> {
|
|||
self.format_duration(duration)
|
||||
));
|
||||
|
||||
lines.push(self.colored(&"═".repeat(60), Color::Blue).clone());
|
||||
lines.push(format!("{}", self.colored(&"═".repeat(60), Color::Blue)));
|
||||
}
|
||||
|
||||
lines
|
||||
|
|
@ -508,19 +491,19 @@ impl<W: Write> Display<W> {
|
|||
let mut host_counts: HashMap<String, (usize, usize, usize, usize)> =
|
||||
HashMap::new();
|
||||
|
||||
for build in state.full_summary.running_builds.values() {
|
||||
for (_, build) in &state.full_summary.running_builds {
|
||||
let host = build.host.name().to_string();
|
||||
let entry = host_counts.entry(host).or_insert((0, 0, 0, 0));
|
||||
entry.0 += 1;
|
||||
}
|
||||
|
||||
for build in state.full_summary.completed_builds.values() {
|
||||
for (_, build) in &state.full_summary.completed_builds {
|
||||
let host = build.host.name().to_string();
|
||||
let entry = host_counts.entry(host).or_insert((0, 0, 0, 0));
|
||||
entry.1 += 1;
|
||||
}
|
||||
|
||||
for build in state.full_summary.failed_builds.values() {
|
||||
for (_, build) in &state.full_summary.failed_builds {
|
||||
let host = build.host.name().to_string();
|
||||
let entry = host_counts.entry(host).or_insert((0, 0, 0, 0));
|
||||
entry.2 += 1;
|
||||
|
|
@ -534,10 +517,9 @@ impl<W: Write> Display<W> {
|
|||
));
|
||||
|
||||
// Summary line
|
||||
let summary_prefix = if has_tree { "┗━" } else { "━" };
|
||||
lines.push(format!(
|
||||
"{} ∑ {} {} │ {} {} │ {} {} │ {} {} │ {} {}",
|
||||
self.colored(summary_prefix, Color::Blue),
|
||||
self.colored("━", Color::Blue),
|
||||
self.colored("⏵", Color::Yellow),
|
||||
running,
|
||||
self.colored("✔", Color::Green),
|
||||
|
|
@ -567,10 +549,9 @@ impl<W: Write> Display<W> {
|
|||
let planned = state.full_summary.planned_builds.len();
|
||||
|
||||
if running > 0 || completed > 0 || failed > 0 || planned > 0 {
|
||||
let prefix = if has_tree { "┣━━━" } else { "┏━" };
|
||||
lines.push(format!(
|
||||
"{} Build Summary:",
|
||||
self.colored(prefix, Color::Blue)
|
||||
self.colored("┣━━━", Color::Blue)
|
||||
));
|
||||
lines.push(format!(
|
||||
"┃ {} Running: {running}",
|
||||
|
|
@ -638,14 +619,7 @@ impl<W: Write> Display<W> {
|
|||
|
||||
// Always show progress line, even if empty
|
||||
if running > 0 || planned > 0 || downloading > 0 || uploading > 0 {
|
||||
let progress_line = if progress_parts.is_empty() {
|
||||
format!(
|
||||
"{} {} {}",
|
||||
self.colored("━", Color::Blue),
|
||||
self.colored("⏱", Color::Grey),
|
||||
self.format_duration(duration)
|
||||
)
|
||||
} else {
|
||||
let progress_line = if !progress_parts.is_empty() {
|
||||
format!(
|
||||
"{} {} {} {}",
|
||||
self.colored("━", Color::Blue),
|
||||
|
|
@ -653,6 +627,13 @@ impl<W: Write> Display<W> {
|
|||
progress_parts.join(" "),
|
||||
self.format_duration(duration)
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{} {} {}",
|
||||
self.colored("━", Color::Blue),
|
||||
self.colored("⏱", Color::Grey),
|
||||
self.format_duration(duration)
|
||||
)
|
||||
};
|
||||
lines.push(progress_line);
|
||||
}
|
||||
|
|
@ -698,23 +679,11 @@ impl<W: Write> Display<W> {
|
|||
if let Some(info) = state.get_derivation_info(*drv_id) {
|
||||
let name = &info.name.name;
|
||||
let elapsed = current_time() - build.start;
|
||||
|
||||
// Format time info
|
||||
let mut time_info = String::new();
|
||||
if let Some(estimate_secs) = build.estimate {
|
||||
let remaining = estimate_secs.saturating_sub(elapsed as u64);
|
||||
time_info.push_str(&format!(
|
||||
"∅ {} ",
|
||||
self.format_duration(remaining as f64)
|
||||
));
|
||||
}
|
||||
time_info.push_str(&self.format_duration(elapsed));
|
||||
|
||||
lines.push(format!(
|
||||
" {} {} {}",
|
||||
self.colored("⏵", Color::Yellow),
|
||||
name,
|
||||
time_info
|
||||
self.format_duration(elapsed)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -733,7 +702,7 @@ impl<W: Write> Display<W> {
|
|||
|
||||
if let Some(build_info) = primary_build {
|
||||
let name = &build_info.name.name;
|
||||
lines.push(format!("BUILD GRAPH: {name}"));
|
||||
lines.push(format!("BUILD GRAPH: {}", name));
|
||||
lines.push("─".repeat(44));
|
||||
|
||||
// Get host information from running/completed builds
|
||||
|
|
@ -768,8 +737,8 @@ impl<W: Write> Display<W> {
|
|||
let duration = current_time() - state.start_time;
|
||||
|
||||
// Format dashboard
|
||||
lines.push(format!("Host │ {host}"));
|
||||
lines.push(format!("Status │ {status}"));
|
||||
lines.push(format!("Host │ {}", host));
|
||||
lines.push(format!("Status │ {}", status));
|
||||
lines.push(format!("Duration │ {}", self.format_duration(duration)));
|
||||
lines.push("─".repeat(44));
|
||||
|
||||
|
|
@ -798,7 +767,7 @@ impl<W: Write> Display<W> {
|
|||
|
||||
if let Some(build_info) = primary_build {
|
||||
let name = &build_info.name.name;
|
||||
lines.push(format!("BUILD GRAPH: {name}"));
|
||||
lines.push(format!("BUILD GRAPH: {}", name));
|
||||
lines.push("─".repeat(44));
|
||||
|
||||
// Get host from build reports or completed builds
|
||||
|
|
@ -829,8 +798,8 @@ impl<W: Write> Display<W> {
|
|||
|
||||
let duration = current_time() - state.start_time;
|
||||
|
||||
lines.push(format!("Host │ {host}"));
|
||||
lines.push(format!("Status │ {status}"));
|
||||
lines.push(format!("Host │ {}", host));
|
||||
lines.push(format!("Status │ {}", status));
|
||||
lines.push(format!("Duration │ {}", self.format_duration(duration)));
|
||||
lines.push("─".repeat(44));
|
||||
|
||||
|
|
@ -891,6 +860,28 @@ impl<W: Write> Display<W> {
|
|||
lines
|
||||
}
|
||||
|
||||
fn is_active_or_has_active_descendants(
|
||||
&self,
|
||||
state: &State,
|
||||
drv_id: DerivationId,
|
||||
) -> bool {
|
||||
if let Some(info) = state.get_derivation_info(drv_id) {
|
||||
match info.build_status {
|
||||
BuildStatus::Building(_) => return true,
|
||||
BuildStatus::Failed { .. } => return true,
|
||||
_ => {},
|
||||
}
|
||||
|
||||
// Check children
|
||||
for input in &info.input_derivations {
|
||||
if self.is_active_or_has_active_descendants(state, input.derivation) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn build_active_forest(
|
||||
&self,
|
||||
state: &State,
|
||||
|
|
@ -984,19 +975,8 @@ impl<W: Write> Display<W> {
|
|||
}
|
||||
}
|
||||
|
||||
// Time information
|
||||
// Time elapsed
|
||||
let elapsed = current_time() - build_info.start;
|
||||
|
||||
// Show estimate if available
|
||||
if let Some(estimate_secs) = build_info.estimate {
|
||||
let remaining = estimate_secs.saturating_sub(elapsed as u64);
|
||||
line.push_str(&self.colored(
|
||||
&format!(" ∅ {}", self.format_duration(remaining as f64)),
|
||||
Color::DarkGrey,
|
||||
));
|
||||
}
|
||||
|
||||
// Show elapsed time
|
||||
line.push_str(&self.colored(
|
||||
&format!(" ⏱ {}", self.format_duration(elapsed)),
|
||||
Color::DarkGrey,
|
||||
|
|
@ -1066,7 +1046,7 @@ impl<W: Write> Display<W> {
|
|||
|
||||
pub fn format_duration(&self, secs: f64) -> String {
|
||||
if secs < 60.0 {
|
||||
format!("{secs:.0}s")
|
||||
format!("{:.0}s", secs)
|
||||
} else if secs < 3600.0 {
|
||||
format!("{:.0}m{:.0}s", secs / 60.0, secs % 60.0)
|
||||
} else {
|
||||
|
|
@ -1085,7 +1065,7 @@ impl<W: Write> Display<W> {
|
|||
fn format_bytes(&self, bytes: u64, total: u64) -> String {
|
||||
let format_size = |b: u64| -> String {
|
||||
if b < 1024 {
|
||||
format!("{b} B")
|
||||
format!("{} B", b)
|
||||
} else if b < 1024 * 1024 {
|
||||
format!("{:.1} KB", b as f64 / 1024.0)
|
||||
} else if b < 1024 * 1024 * 1024 {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
//! ROM - Rust Output Monitor
|
||||
pub mod cache;
|
||||
pub mod cli;
|
||||
pub mod display;
|
||||
pub mod error;
|
||||
|
|
|
|||
|
|
@ -5,21 +5,10 @@ use std::{
|
|||
time::Duration,
|
||||
};
|
||||
|
||||
use cognos::Host;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{
|
||||
cache::BuildReportCache,
|
||||
display::{Display, DisplayConfig, LegendStyle, SummaryStyle},
|
||||
display::{Display, DisplayConfig},
|
||||
error::{Result, RomError},
|
||||
state::{
|
||||
BuildStatus,
|
||||
Derivation,
|
||||
FailType,
|
||||
State,
|
||||
StorePath,
|
||||
StorePathState,
|
||||
},
|
||||
state::State,
|
||||
types::{Config, InputMode},
|
||||
update,
|
||||
};
|
||||
|
|
@ -35,15 +24,15 @@ impl<W: Write> Monitor<W> {
|
|||
/// Create a new monitor
|
||||
pub fn new(config: Config, writer: W) -> Result<Self> {
|
||||
let legend_style = match config.legend_style.to_lowercase().as_str() {
|
||||
"compact" => LegendStyle::Compact,
|
||||
"verbose" => LegendStyle::Verbose,
|
||||
_ => LegendStyle::Table,
|
||||
"compact" => crate::display::LegendStyle::Compact,
|
||||
"verbose" => crate::display::LegendStyle::Verbose,
|
||||
_ => crate::display::LegendStyle::Table,
|
||||
};
|
||||
|
||||
let summary_style = match config.summary_style.to_lowercase().as_str() {
|
||||
"table" => SummaryStyle::Table,
|
||||
"full" => SummaryStyle::Full,
|
||||
_ => SummaryStyle::Concise,
|
||||
"table" => crate::display::SummaryStyle::Table,
|
||||
"full" => crate::display::SummaryStyle::Full,
|
||||
_ => crate::display::SummaryStyle::Concise,
|
||||
};
|
||||
|
||||
let display_config = DisplayConfig {
|
||||
|
|
@ -57,12 +46,7 @@ impl<W: Write> Monitor<W> {
|
|||
};
|
||||
|
||||
let display = Display::new(writer, display_config)?;
|
||||
let mut state = State::new();
|
||||
|
||||
// Load build cache for predictions
|
||||
let cache_path = BuildReportCache::default_cache_path();
|
||||
let cache = BuildReportCache::new(cache_path);
|
||||
state.build_cache = cache.load();
|
||||
let state = State::new();
|
||||
|
||||
Ok(Self {
|
||||
state,
|
||||
|
|
@ -98,14 +82,6 @@ impl<W: Write> Monitor<W> {
|
|||
self.display.render_final(&self.state)?;
|
||||
}
|
||||
|
||||
// Save build cache for future predictions
|
||||
let cache_path = BuildReportCache::default_cache_path();
|
||||
let cache = BuildReportCache::new(cache_path);
|
||||
if let Err(e) = cache.save(&self.state.build_cache) {
|
||||
debug!("Failed to save build cache: {}", e);
|
||||
// Don't fail the build if cache save fails
|
||||
}
|
||||
|
||||
// Return error code if there were failures
|
||||
if self.state.has_errors() {
|
||||
return Err(RomError::BuildFailed);
|
||||
|
|
@ -135,7 +111,7 @@ impl<W: Write> Monitor<W> {
|
|||
Ok(action) => {
|
||||
// Handle message passthrough - print directly to stdout
|
||||
if let cognos::Actions::Message { msg, .. } = &action {
|
||||
println!("{msg}");
|
||||
println!("{}", msg);
|
||||
}
|
||||
|
||||
let changed = update::process_message(&mut self.state, action);
|
||||
|
|
@ -149,13 +125,17 @@ impl<W: Write> Monitor<W> {
|
|||
}
|
||||
} else {
|
||||
// Non-JSON lines in JSON mode are passed through
|
||||
println!("{line}");
|
||||
println!("{}", line);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Process a human-readable line
|
||||
fn process_human_line(&mut self, line: &str) -> Result<bool> {
|
||||
// Parse human-readable nix output
|
||||
// This is a simplified version - the full implementation would need
|
||||
// comprehensive parsing of nix's output format
|
||||
|
||||
let line = line.trim();
|
||||
|
||||
// Skip empty lines
|
||||
|
|
@ -172,7 +152,7 @@ impl<W: Write> Monitor<W> {
|
|||
|
||||
let build_info = crate::state::BuildInfo {
|
||||
start: now,
|
||||
host: Host::Localhost,
|
||||
host: crate::state::Host::Localhost,
|
||||
estimate: None,
|
||||
activity_id: None,
|
||||
};
|
||||
|
|
@ -193,21 +173,20 @@ impl<W: Write> Monitor<W> {
|
|||
let path_id = self.state.get_or_create_store_path_id(path);
|
||||
let now = crate::state::current_time();
|
||||
|
||||
// Try to extract byte size from the message
|
||||
let total_bytes = extract_byte_size(line);
|
||||
|
||||
let transfer = crate::state::TransferInfo {
|
||||
start: now,
|
||||
host: Host::Localhost,
|
||||
activity_id: 0, // no activity ID in human mode
|
||||
start: now,
|
||||
host: crate::state::Host::Localhost,
|
||||
activity_id: 0, // No activity ID in human mode
|
||||
bytes_transferred: 0,
|
||||
total_bytes,
|
||||
total_bytes: None,
|
||||
};
|
||||
|
||||
if let Some(path_info) = self.state.get_store_path_info_mut(path_id) {
|
||||
path_info
|
||||
.states
|
||||
.insert(StorePathState::Downloading(transfer.clone()));
|
||||
.insert(crate::state::StorePathState::Downloading(
|
||||
transfer.clone(),
|
||||
));
|
||||
}
|
||||
|
||||
self
|
||||
|
|
@ -221,113 +200,14 @@ impl<W: Write> Monitor<W> {
|
|||
}
|
||||
}
|
||||
|
||||
// Detect download completions with byte sizes
|
||||
if line.starts_with("downloaded") || line.contains("downloaded '") {
|
||||
if let Some(path_str) = extract_path_from_message(line) {
|
||||
if let Some(path) = StorePath::parse(&path_str) {
|
||||
if let Some(&path_id) = self.state.store_path_ids.get(&path) {
|
||||
let now = crate::state::current_time();
|
||||
let total_bytes = extract_byte_size(line).unwrap_or(0);
|
||||
|
||||
// Get start time from running download if it exists
|
||||
let start = self
|
||||
.state
|
||||
.full_summary
|
||||
.running_downloads
|
||||
.get(&path_id)
|
||||
.map_or(now, |t| t.start);
|
||||
|
||||
let completed = crate::state::CompletedTransferInfo {
|
||||
start,
|
||||
end: now,
|
||||
host: Host::Localhost,
|
||||
total_bytes,
|
||||
};
|
||||
|
||||
if let Some(path_info) = self.state.get_store_path_info_mut(path_id)
|
||||
{
|
||||
path_info
|
||||
.states
|
||||
.insert(StorePathState::Downloaded(completed.clone()));
|
||||
}
|
||||
|
||||
self.state.full_summary.running_downloads.remove(&path_id);
|
||||
self
|
||||
.state
|
||||
.full_summary
|
||||
.completed_downloads
|
||||
.insert(path_id, completed);
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detect "checking outputs of" messages
|
||||
if line.contains("checking outputs of") {
|
||||
if let Some(drv_path) = extract_path_from_message(line) {
|
||||
if let Some(drv) = crate::state::Derivation::parse(&drv_path) {
|
||||
let drv_id = self.state.get_or_create_derivation_id(drv);
|
||||
// Just mark it as "touched" - checking happens after build
|
||||
// Reminds me of Sako...
|
||||
self.state.touched_ids.insert(drv_id);
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detect "copying N paths" messages
|
||||
if line.starts_with("copying") && line.contains("paths") {
|
||||
// Extract number of paths if present
|
||||
let words: Vec<&str> = line.split_whitespace().collect();
|
||||
if words.len() >= 2 {
|
||||
if let Ok(count) = words[1].parse::<usize>() {
|
||||
debug!("Copying {} paths", count);
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detect errors
|
||||
if line.starts_with("error:") || line.contains("error:") {
|
||||
self.state.nix_errors.push(line.to_string());
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Try to determine the error type and associated derivation
|
||||
let fail_type = if line.contains("hash mismatch")
|
||||
|| line.contains("output path")
|
||||
&& (line.contains("hash") || line.contains("differs"))
|
||||
{
|
||||
FailType::HashMismatch
|
||||
} else if line.contains("timed out") || line.contains("timeout") {
|
||||
FailType::Timeout
|
||||
} else if line.contains("dependency failed")
|
||||
|| line.contains("dependencies failed")
|
||||
{
|
||||
FailType::DependencyFailed
|
||||
} else if line.contains("builder for")
|
||||
&& line.contains("failed with exit code")
|
||||
{
|
||||
// Try to extract exit code
|
||||
if let Some(code_pos) = line.find("exit code") {
|
||||
let after_code = &line[code_pos + 10..];
|
||||
let code_str = after_code
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.map(|s| s.trim_end_matches(|c: char| !c.is_ascii_digit()));
|
||||
if let Some(code) = code_str.and_then(|s| s.parse::<i32>().ok()) {
|
||||
FailType::BuildFailed(code)
|
||||
} else {
|
||||
FailType::Unknown
|
||||
}
|
||||
} else {
|
||||
FailType::Unknown
|
||||
}
|
||||
} else {
|
||||
FailType::Unknown
|
||||
};
|
||||
|
||||
// Try to find the associated derivation and mark it as failed
|
||||
// Detect build completions
|
||||
if line.starts_with("built") || line.contains("built '") {
|
||||
if let Some(drv_path) = extract_path_from_message(line) {
|
||||
if let Some(drv) = crate::state::Derivation::parse(&drv_path) {
|
||||
if let Some(&drv_id) = self.state.derivation_ids.get(&drv) {
|
||||
|
|
@ -338,35 +218,11 @@ impl<W: Write> Monitor<W> {
|
|||
let now = crate::state::current_time();
|
||||
self.state.update_build_status(
|
||||
drv_id,
|
||||
crate::state::BuildStatus::Failed {
|
||||
crate::state::BuildStatus::Built {
|
||||
info: build_info.clone(),
|
||||
fail: crate::state::BuildFail {
|
||||
at: now,
|
||||
fail_type: fail_type.clone(),
|
||||
},
|
||||
end: now,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Detect build completions
|
||||
if line.starts_with("built") || line.contains("built '") {
|
||||
if let Some(drv_path) = extract_path_from_message(line) {
|
||||
if let Some(drv) = Derivation::parse(&drv_path) {
|
||||
if let Some(&drv_id) = self.state.derivation_ids.get(&drv) {
|
||||
if let Some(info) = self.state.get_derivation_info(drv_id) {
|
||||
if let BuildStatus::Building(build_info) = &info.build_status {
|
||||
let now = crate::state::current_time();
|
||||
self.state.update_build_status(drv_id, BuildStatus::Built {
|
||||
info: build_info.clone(),
|
||||
end: now,
|
||||
});
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -414,33 +270,6 @@ fn extract_path_from_message(line: &str) -> Option<String> {
|
|||
None
|
||||
}
|
||||
|
||||
/// Extract byte size from a message line (e.g., "downloaded 123 KiB")
|
||||
fn extract_byte_size(line: &str) -> Option<u64> {
|
||||
// Look for patterns like "123 KiB", "6.7 MiB", etc.
|
||||
// Haha 6.7
|
||||
let words: Vec<&str> = line.split_whitespace().collect();
|
||||
for (i, word) in words.iter().enumerate() {
|
||||
if i + 1 < words.len() {
|
||||
let unit = words[i + 1];
|
||||
if matches!(unit, "B" | "KiB" | "MiB" | "GiB" | "TiB" | "PiB") {
|
||||
if let Ok(value) = word.parse::<f64>() {
|
||||
let multiplier = match unit {
|
||||
"B" => 1_u64,
|
||||
"KiB" => 1024,
|
||||
"MiB" => 1024 * 1024,
|
||||
"GiB" => 1024 * 1024 * 1024,
|
||||
"TiB" => 1024_u64 * 1024 * 1024 * 1024,
|
||||
"PiB" => 1024_u64 * 1024 * 1024 * 1024 * 1024,
|
||||
_ => 1,
|
||||
};
|
||||
return Some((value * multiplier as f64) as u64);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -467,19 +296,4 @@ mod tests {
|
|||
let path = extract_path_from_message(line);
|
||||
assert!(path.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_byte_size() {
|
||||
let line = "downloaded 123 KiB in 2 seconds";
|
||||
assert_eq!(extract_byte_size(line), Some(123 * 1024));
|
||||
|
||||
let line2 = "downloading 4.5 MiB";
|
||||
assert_eq!(
|
||||
extract_byte_size(line2),
|
||||
Some((4.5 * 1024.0 * 1024.0) as u64)
|
||||
);
|
||||
|
||||
let line3 = "no size here";
|
||||
assert_eq!(extract_byte_size(line3), None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
225
rom/src/state.rs
225
rom/src/state.rs
|
|
@ -6,7 +6,7 @@ use std::{
|
|||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use cognos::{Host, Id, OutputName, ProgressState};
|
||||
use cognos::Id;
|
||||
use indexmap::IndexMap;
|
||||
|
||||
/// Unique identifier for store paths
|
||||
|
|
@ -18,6 +18,36 @@ pub type DerivationId = usize;
|
|||
/// Unique identifier for activities
|
||||
pub type ActivityId = Id;
|
||||
|
||||
/// Overall progress state
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ProgressState {
|
||||
JustStarted,
|
||||
InputReceived,
|
||||
Finished,
|
||||
}
|
||||
|
||||
/// Build host information
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Host {
|
||||
Localhost,
|
||||
Remote(String),
|
||||
}
|
||||
|
||||
impl Host {
|
||||
#[must_use]
|
||||
pub const fn is_local(&self) -> bool {
|
||||
matches!(self, Self::Localhost)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
Self::Localhost => "localhost",
|
||||
Self::Remote(name) => name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Store path representation
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct StorePath {
|
||||
|
|
@ -81,6 +111,37 @@ impl Derivation {
|
|||
}
|
||||
}
|
||||
|
||||
/// Output name for derivations
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum OutputName {
|
||||
Out,
|
||||
Doc,
|
||||
Dev,
|
||||
Bin,
|
||||
Info,
|
||||
Lib,
|
||||
Man,
|
||||
Dist,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl OutputName {
|
||||
#[must_use]
|
||||
pub fn parse(name: &str) -> Self {
|
||||
match name.to_lowercase().as_str() {
|
||||
"out" => Self::Out,
|
||||
"doc" => Self::Doc,
|
||||
"dev" => Self::Dev,
|
||||
"bin" => Self::Bin,
|
||||
"info" => Self::Info,
|
||||
"lib" => Self::Lib,
|
||||
"man" => Self::Man,
|
||||
"dist" => Self::Dist,
|
||||
_ => Self::Other(name.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Transfer information (download/upload)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TransferInfo {
|
||||
|
|
@ -317,20 +378,6 @@ pub struct ActivityStatus {
|
|||
pub text: String,
|
||||
pub parent: Option<ActivityId>,
|
||||
pub phase: Option<String>,
|
||||
pub progress: Option<ActivityProgress>,
|
||||
}
|
||||
|
||||
/// Activity progress for downloads/uploads/builds
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ActivityProgress {
|
||||
/// Bytes completed
|
||||
pub done: u64,
|
||||
/// Total bytes expected
|
||||
pub expected: u64,
|
||||
/// Currently running transfers
|
||||
pub running: u64,
|
||||
/// Failed transfers
|
||||
pub failed: u64,
|
||||
}
|
||||
|
||||
/// Build report for caching
|
||||
|
|
@ -360,7 +407,6 @@ pub struct State {
|
|||
pub full_summary: DependencySummary,
|
||||
pub forest_roots: Vec<DerivationId>,
|
||||
pub build_reports: HashMap<String, Vec<BuildReport>>,
|
||||
pub build_cache: HashMap<(String, String), Vec<BuildReport>>,
|
||||
pub start_time: f64,
|
||||
pub progress_state: ProgressState,
|
||||
pub store_path_ids: HashMap<StorePath, StorePathId>,
|
||||
|
|
@ -369,11 +415,8 @@ pub struct State {
|
|||
pub activities: HashMap<ActivityId, ActivityStatus>,
|
||||
pub nix_errors: Vec<String>,
|
||||
pub build_logs: Vec<String>,
|
||||
pub traces: Vec<String>,
|
||||
pub build_platform: Option<String>,
|
||||
pub evaluation_state: EvalInfo,
|
||||
pub builds_activity: Option<ActivityId>,
|
||||
pub success_tokens: u64,
|
||||
next_store_path_id: StorePathId,
|
||||
next_derivation_id: DerivationId,
|
||||
}
|
||||
|
|
@ -393,7 +436,6 @@ impl State {
|
|||
full_summary: DependencySummary::default(),
|
||||
forest_roots: Vec::new(),
|
||||
build_reports: HashMap::new(),
|
||||
build_cache: HashMap::new(),
|
||||
start_time: current_time(),
|
||||
progress_state: ProgressState::JustStarted,
|
||||
store_path_ids: HashMap::new(),
|
||||
|
|
@ -402,11 +444,8 @@ impl State {
|
|||
activities: HashMap::new(),
|
||||
nix_errors: Vec::new(),
|
||||
build_logs: Vec::new(),
|
||||
traces: Vec::new(),
|
||||
build_platform: None,
|
||||
evaluation_state: EvalInfo::default(),
|
||||
builds_activity: None,
|
||||
success_tokens: 0,
|
||||
next_store_path_id: 0,
|
||||
next_derivation_id: 0,
|
||||
}
|
||||
|
|
@ -564,7 +603,7 @@ impl State {
|
|||
// Create output set
|
||||
let mut output_set = HashSet::new();
|
||||
for output in outputs {
|
||||
output_set.insert(OutputName::parse(&output));
|
||||
output_set.insert(parse_output_name(&output));
|
||||
}
|
||||
|
||||
// Add to parent's input derivations
|
||||
|
|
@ -679,130 +718,6 @@ impl State {
|
|||
.map(|(id, info)| (*id, info))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Check if a derivation has a platform mismatch
|
||||
#[must_use]
|
||||
pub fn has_platform_mismatch(&self, id: DerivationId) -> bool {
|
||||
if let (Some(build_platform), Some(info)) =
|
||||
(&self.build_platform, self.get_derivation_info(id))
|
||||
{
|
||||
if let Some(drv_platform) = &info.platform {
|
||||
return build_platform != drv_platform;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Get all derivations with platform mismatches
|
||||
#[must_use]
|
||||
pub fn platform_mismatches(&self) -> Vec<DerivationId> {
|
||||
self
|
||||
.derivation_infos
|
||||
.keys()
|
||||
.filter(|&&id| self.has_platform_mismatch(id))
|
||||
.copied()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get the activity prefix for a given activity ID by walking up the parent
|
||||
/// chain to find a Build activity and extracting its derivation name.
|
||||
/// Returns a prefix like "hello> " suitable for prepending to log lines.
|
||||
/// If `use_color` is true and stderr is a TTY, the prefix will be blue.
|
||||
/// The `prefix_style` determines whether to use short (pname only), full, or
|
||||
/// no prefix.
|
||||
#[must_use]
|
||||
pub fn get_activity_prefix(
|
||||
&self,
|
||||
activity_id: ActivityId,
|
||||
prefix_style: &crate::types::LogPrefixStyle,
|
||||
use_color: bool,
|
||||
) -> Option<String> {
|
||||
use cognos::Activities;
|
||||
|
||||
use crate::types::LogPrefixStyle;
|
||||
|
||||
// If prefix style is None, return empty string
|
||||
if matches!(prefix_style, LogPrefixStyle::None) {
|
||||
return Some(String::new());
|
||||
}
|
||||
|
||||
let mut current_id = activity_id;
|
||||
let max_depth = 10; // Prevent infinite loops
|
||||
let mut depth = 0;
|
||||
|
||||
while depth < max_depth {
|
||||
if let Some(activity) = self.activities.get(¤t_id) {
|
||||
// Check if this is a Build activity (type 105)
|
||||
if activity.activity == Activities::Build as u8 {
|
||||
// Extract derivation path from the text field
|
||||
// The text field typically contains something like:
|
||||
// "building '/nix/store/...-hello-2.10.drv'"
|
||||
if let Some(drv) = extract_derivation_from_text(&activity.text) {
|
||||
// Look up the DerivationInfo for this derivation
|
||||
let drv_id = self.derivation_ids.get(&drv);
|
||||
let name = if matches!(prefix_style, LogPrefixStyle::Short) {
|
||||
// Try to use pname if available
|
||||
if let Some(id) = drv_id {
|
||||
if let Some(drv_info) = self.derivation_infos.get(id) {
|
||||
if let Some(pname) = &drv_info.pname {
|
||||
pname.clone()
|
||||
} else {
|
||||
drv.name.clone()
|
||||
}
|
||||
} else {
|
||||
drv.name.clone()
|
||||
}
|
||||
} else {
|
||||
drv.name.clone()
|
||||
}
|
||||
} else {
|
||||
// Full style - use full derivation name
|
||||
drv.name.clone()
|
||||
};
|
||||
|
||||
// Apply color if requested and stderr is a TTY
|
||||
let colored_name = if use_color
|
||||
&& std::io::IsTerminal::is_terminal(&std::io::stderr())
|
||||
{
|
||||
format!("\x1b[34m{name}\x1b[0m")
|
||||
} else {
|
||||
name
|
||||
};
|
||||
|
||||
return Some(format!("{colored_name}> "));
|
||||
}
|
||||
}
|
||||
|
||||
// Move to parent activity
|
||||
if let Some(parent_id) = activity.parent {
|
||||
if parent_id == 0 {
|
||||
break; // Reached root
|
||||
}
|
||||
current_id = parent_id;
|
||||
depth += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract derivation from activity text like "building
|
||||
/// '/nix/store/...-hello-2.10.drv'" Returns the Derivation object
|
||||
fn extract_derivation_from_text(text: &str) -> Option<Derivation> {
|
||||
// Look for .drv path in text
|
||||
if let Some(start) = text.find("/nix/store/") {
|
||||
if let Some(end) = text[start..].find(".drv") {
|
||||
let drv_path = &text[start..start + end + 4]; // Include .drv
|
||||
return Derivation::parse(drv_path);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
|
@ -813,6 +728,20 @@ pub fn current_time() -> f64 {
|
|||
.as_secs_f64()
|
||||
}
|
||||
|
||||
fn parse_output_name(name: &str) -> OutputName {
|
||||
match name {
|
||||
"out" => OutputName::Out,
|
||||
"doc" => OutputName::Doc,
|
||||
"dev" => OutputName::Dev,
|
||||
"bin" => OutputName::Bin,
|
||||
"info" => OutputName::Info,
|
||||
"lib" => OutputName::Lib,
|
||||
"man" => OutputName::Man,
|
||||
"dist" => OutputName::Dist,
|
||||
_ => OutputName::Other(name.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -11,17 +11,6 @@ pub enum DisplayFormat {
|
|||
Dashboard,
|
||||
}
|
||||
|
||||
/// Log prefix style for build logs
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum LogPrefixStyle {
|
||||
/// Just package name (pname)
|
||||
Short,
|
||||
/// Full derivation name with version
|
||||
Full,
|
||||
/// No prefix
|
||||
None,
|
||||
}
|
||||
|
||||
/// Summary display style
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SummaryStyle {
|
||||
|
|
@ -34,7 +23,6 @@ pub enum SummaryStyle {
|
|||
}
|
||||
|
||||
impl SummaryStyle {
|
||||
#[must_use]
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"concise" => Self::Concise,
|
||||
|
|
@ -45,20 +33,7 @@ impl SummaryStyle {
|
|||
}
|
||||
}
|
||||
|
||||
impl LogPrefixStyle {
|
||||
#[must_use]
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"short" => Self::Short,
|
||||
"full" => Self::Full,
|
||||
"none" => Self::None,
|
||||
_ => Self::Short,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DisplayFormat {
|
||||
#[must_use]
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"tree" => Self::Tree,
|
||||
|
|
@ -73,40 +48,34 @@ impl DisplayFormat {
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
/// Whether we're piping output through
|
||||
pub piping: bool,
|
||||
pub piping: bool,
|
||||
/// Silent mode - minimal output
|
||||
pub silent: bool,
|
||||
pub silent: bool,
|
||||
/// Input parsing mode
|
||||
pub input_mode: InputMode,
|
||||
pub input_mode: InputMode,
|
||||
/// Show completion times
|
||||
pub show_timers: bool,
|
||||
pub show_timers: bool,
|
||||
/// Terminal width override
|
||||
pub width: Option<usize>,
|
||||
pub width: Option<usize>,
|
||||
/// Display format
|
||||
pub format: DisplayFormat,
|
||||
pub format: DisplayFormat,
|
||||
/// Legend display style
|
||||
pub legend_style: String,
|
||||
pub legend_style: String,
|
||||
/// Summary display style
|
||||
pub summary_style: String,
|
||||
/// Log prefix style for build logs
|
||||
pub log_prefix_style: LogPrefixStyle,
|
||||
/// Maximum number of log lines to display (None = unlimited)
|
||||
pub log_line_limit: Option<usize>,
|
||||
pub summary_style: String,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
piping: false,
|
||||
silent: false,
|
||||
input_mode: InputMode::Human,
|
||||
show_timers: true,
|
||||
width: None,
|
||||
format: DisplayFormat::Tree,
|
||||
legend_style: "table".to_string(),
|
||||
summary_style: "concise".to_string(),
|
||||
log_prefix_style: LogPrefixStyle::Short,
|
||||
log_line_limit: None,
|
||||
piping: false,
|
||||
silent: false,
|
||||
input_mode: InputMode::Human,
|
||||
show_timers: true,
|
||||
width: None,
|
||||
format: DisplayFormat::Tree,
|
||||
legend_style: "table".to_string(),
|
||||
summary_style: "concise".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -132,8 +101,6 @@ mod tests {
|
|||
assert_eq!(config.input_mode, InputMode::Human);
|
||||
assert!(config.show_timers);
|
||||
assert_eq!(config.format, DisplayFormat::Tree);
|
||||
assert_eq!(config.log_prefix_style, LogPrefixStyle::Short);
|
||||
assert_eq!(config.log_line_limit, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1,31 +1,29 @@
|
|||
//! State update logic for processing nix messages
|
||||
|
||||
use cognos::{Actions, Activities, Host, Id, ProgressState, Verbosity};
|
||||
use cognos::{Actions, Activities, Id, Verbosity};
|
||||
use tracing::{debug, trace};
|
||||
|
||||
use crate::{
|
||||
cache::BuildReportCache,
|
||||
state::{
|
||||
ActivityProgress,
|
||||
ActivityStatus,
|
||||
BuildFail,
|
||||
BuildInfo,
|
||||
BuildReport,
|
||||
BuildStatus,
|
||||
CompletedBuildInfo,
|
||||
CompletedTransferInfo,
|
||||
Derivation,
|
||||
DerivationId,
|
||||
FailType,
|
||||
FailedBuildInfo,
|
||||
InputDerivation,
|
||||
State,
|
||||
StorePath,
|
||||
StorePathId,
|
||||
StorePathState,
|
||||
TransferInfo,
|
||||
current_time,
|
||||
},
|
||||
use crate::state::{
|
||||
ActivityStatus,
|
||||
BuildFail,
|
||||
BuildInfo,
|
||||
BuildStatus,
|
||||
CompletedBuildInfo,
|
||||
CompletedTransferInfo,
|
||||
Derivation,
|
||||
DerivationId,
|
||||
FailType,
|
||||
FailedBuildInfo,
|
||||
Host,
|
||||
InputDerivation,
|
||||
OutputName,
|
||||
ProgressState,
|
||||
State,
|
||||
StorePath,
|
||||
StorePathId,
|
||||
StorePathState,
|
||||
TransferInfo,
|
||||
current_time,
|
||||
};
|
||||
|
||||
/// Process a nix JSON message and update state
|
||||
|
|
@ -91,38 +89,21 @@ fn handle_start(
|
|||
text: text.clone(),
|
||||
parent: parent_id,
|
||||
phase: None,
|
||||
progress: None,
|
||||
});
|
||||
|
||||
let changed = match activity_u8 {
|
||||
105 => handle_build_start(state, id, parent_id, &text, &fields, now), /* Build */
|
||||
104 | 105 => handle_build_start(state, id, parent_id, &text, &fields, now), /* Builds | Build */
|
||||
108 => handle_substitute_start(state, id, &text, &fields, now), /* Substitute */
|
||||
109 => handle_query_path_info_start(state, id, &text, &fields, now), /* QueryPathInfo */
|
||||
110 => handle_post_build_hook_start(state, id, &text, &fields, now), /* PostBuildHook */
|
||||
101 => handle_file_transfer_start(state, id, &text, &fields, now), /* FileTransfer */
|
||||
100 => handle_copy_path_start(state, id, &text, &fields, now), /* CopyPath */
|
||||
104 => {
|
||||
// Builds activity - track this as the top-level builds activity
|
||||
if state.builds_activity.is_none() {
|
||||
state.builds_activity = Some(id);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
102 | 103 | 106 | 107 | 111 | 112 => {
|
||||
// Realise, CopyPaths, OptimiseStore, VerifyPaths, BuildWaiting, FetchTree
|
||||
// These activities have no fields and are just tracked
|
||||
true
|
||||
},
|
||||
_ => {
|
||||
debug!("Unknown activity type: {}", activity_u8);
|
||||
false
|
||||
},
|
||||
101 => handle_transfer_start(state, id, &text, &fields, now, false), /* FileTransfer */
|
||||
100 | 103 => handle_transfer_start(state, id, &text, &fields, now, true), /* CopyPath | CopyPaths */
|
||||
_ => false,
|
||||
};
|
||||
|
||||
// Track parent-child relationships for dependency tree
|
||||
if changed && activity_u8 == 105 && parent_id.is_some() {
|
||||
if changed
|
||||
&& (activity_u8 == 104 || activity_u8 == 105)
|
||||
&& parent_id.is_some()
|
||||
{
|
||||
let parent_act_id = parent_id.unwrap();
|
||||
|
||||
// Find parent and child derivation IDs
|
||||
|
|
@ -132,8 +113,8 @@ fn handle_start(
|
|||
if let Some(parent_drv_id) = parent_drv_id {
|
||||
if let Some(child_drv_id) = child_drv_id {
|
||||
debug!(
|
||||
"Establishing parent-child relationship: parent={parent_drv_id}, \
|
||||
child={child_drv_id}"
|
||||
"Establishing parent-child relationship: parent={}, child={}",
|
||||
parent_drv_id, child_drv_id
|
||||
);
|
||||
|
||||
// Add child as a dependency of parent
|
||||
|
|
@ -172,19 +153,9 @@ fn handle_stop(state: &mut State, id: Id, now: f64) -> bool {
|
|||
state.activities.remove(&id);
|
||||
|
||||
match activity_status.activity {
|
||||
105 => handle_build_stop(state, id, now), // Build
|
||||
108 => handle_substitute_stop(state, id, now), // Substitute
|
||||
101 | 100 => handle_transfer_stop(state, id, now), // FileTransfer,
|
||||
// CopyPath
|
||||
109 | 110 => {
|
||||
// QueryPathInfo, PostBuildHook - just acknowledge stop
|
||||
false
|
||||
},
|
||||
102 | 103 | 104 | 106 | 107 | 111 | 112 => {
|
||||
// Realise, CopyPaths, Builds, OptimiseStore, VerifyPaths, BuildWaiting,
|
||||
// FetchTree
|
||||
false
|
||||
},
|
||||
104 | 105 => handle_build_stop(state, id, now), // Builds | Build
|
||||
108 => handle_substitute_stop(state, id, now), // Substitute
|
||||
101 | 100 | 103 => handle_transfer_stop(state, id, now), /* FileTransfer, CopyPath, CopyPaths */
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
|
|
@ -198,7 +169,7 @@ fn handle_message(state: &mut State, level: Verbosity, msg: String) -> bool {
|
|||
|
||||
// Extract phase from log messages like "Running phase: configurePhase"
|
||||
if let Some(phase_start) = msg.find("Running phase: ") {
|
||||
let phase_name = &msg[phase_start + 15..]; // skip "Running phase: "
|
||||
let phase_name = &msg[phase_start + 15..]; // Skip "Running phase: "
|
||||
let phase = phase_name.trim().to_string();
|
||||
|
||||
// Find the active build and update its phase
|
||||
|
|
@ -260,14 +231,6 @@ fn handle_message(state: &mut State, level: Verbosity, msg: String) -> bool {
|
|||
}
|
||||
true // return true since we stored the log
|
||||
},
|
||||
Verbosity::Talkative
|
||||
| Verbosity::Chatty
|
||||
| Verbosity::Debug
|
||||
| Verbosity::Vomit => {
|
||||
// These are trace-level messages, store separately
|
||||
state.traces.push(msg.clone());
|
||||
true
|
||||
},
|
||||
_ => {
|
||||
true // return true since we stored the log
|
||||
},
|
||||
|
|
@ -277,184 +240,41 @@ fn handle_message(state: &mut State, level: Verbosity, msg: String) -> bool {
|
|||
fn handle_result(
|
||||
state: &mut State,
|
||||
id: Id,
|
||||
result_type: u8,
|
||||
activity: u8,
|
||||
fields: Vec<serde_json::Value>,
|
||||
_now: f64,
|
||||
) -> bool {
|
||||
// Result message types are DIFFERENT from Activity types
|
||||
// Type 100: FileLinked (2 ints)
|
||||
// Type 101: BuildLogLine (1 text)
|
||||
// Type 102: UntrustedPath (1 text - store path)
|
||||
// Type 103: CorruptedPath (1 text - store path)
|
||||
// Type 104: SetPhase (1 text)
|
||||
// Type 105: Progress (4 ints: done, expected, running, failed)
|
||||
// Type 106: SetExpected (2 ints: activity type, count)
|
||||
// Type 107: PostBuildLogLine (1 text)
|
||||
// Type 108: FetchStatus (1 text)
|
||||
|
||||
match result_type {
|
||||
100 => {
|
||||
// FileLinked: 2 int fields (linked count, total count)
|
||||
match activity {
|
||||
101 | 108 => {
|
||||
// FileTransfer or Substitute
|
||||
// Fields contain progress information
|
||||
// XXX: Format: [bytes_transferred, total_bytes]
|
||||
if fields.len() >= 2 {
|
||||
let linked = fields[0].as_u64().unwrap_or(0);
|
||||
let total = fields[1].as_u64().unwrap_or(0);
|
||||
debug!("FileLinked: {}/{}", linked, total);
|
||||
// File linking is reported but doesn't need state tracking
|
||||
}
|
||||
false
|
||||
},
|
||||
101 => {
|
||||
// BuildLogLine: 1 text field
|
||||
if let Some(line) = fields.first().and_then(|f| f.as_str()) {
|
||||
state.build_logs.push(line.to_string());
|
||||
return true;
|
||||
}
|
||||
false
|
||||
},
|
||||
102 => {
|
||||
// UntrustedPath: 1 text field (store path)
|
||||
if let Some(path_str) = fields.first().and_then(|f| f.as_str()) {
|
||||
debug!("Untrusted path reported: {}", path_str);
|
||||
state
|
||||
.nix_errors
|
||||
.push(format!("Untrusted path: {}", path_str));
|
||||
return true;
|
||||
}
|
||||
false
|
||||
},
|
||||
103 => {
|
||||
// CorruptedPath: 1 text field (store path)
|
||||
if let Some(path_str) = fields.first().and_then(|f| f.as_str()) {
|
||||
state.nix_errors.push(format!("Corrupted path: {path_str}"));
|
||||
return true;
|
||||
update_transfer_progress(state, id, &fields);
|
||||
}
|
||||
false
|
||||
},
|
||||
104 => {
|
||||
// SetPhase: 1 text field
|
||||
if let Some(phase_str) = fields.first().and_then(|f| f.as_str()) {
|
||||
if let Some(activity) = state.activities.get_mut(&id) {
|
||||
activity.phase = Some(phase_str.to_string());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
},
|
||||
105 => {
|
||||
// Progress: 4 int fields (done, expected, running, failed)
|
||||
if fields.len() >= 4 {
|
||||
if let (Some(done), Some(expected), Some(running), Some(failed)) = (
|
||||
fields[0].as_u64(),
|
||||
fields[1].as_u64(),
|
||||
fields[2].as_u64(),
|
||||
fields[3].as_u64(),
|
||||
) {
|
||||
// If this progress is for the Builds activity, track success tokens
|
||||
if state.builds_activity == Some(id) {
|
||||
if let Some(activity) = state.activities.get(&id) {
|
||||
if let Some(prev_progress) = &activity.progress {
|
||||
let new_done = done.saturating_sub(prev_progress.done);
|
||||
if new_done > 0 {
|
||||
state.success_tokens =
|
||||
state.success_tokens.saturating_add(new_done);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Builds activity type - contains phase information
|
||||
if !fields.is_empty() {
|
||||
if let Some(phase_str) = fields[0].as_str() {
|
||||
// Update the activity's phase field
|
||||
if let Some(activity) = state.activities.get_mut(&id) {
|
||||
activity.progress = Some(ActivityProgress {
|
||||
done,
|
||||
expected,
|
||||
running,
|
||||
failed,
|
||||
});
|
||||
activity.phase = Some(phase_str.to_string());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
},
|
||||
106 => {
|
||||
// SetExpected: 2 int fields (activity type, count)
|
||||
if fields.len() >= 2 {
|
||||
let activity_type = fields[0].as_u64().unwrap_or(0);
|
||||
let expected_count = fields[1].as_u64().unwrap_or(0);
|
||||
debug!(
|
||||
"SetExpected: activity_type={}, count={}",
|
||||
activity_type, expected_count
|
||||
);
|
||||
// Expected counts are informational and don't affect state tracking
|
||||
}
|
||||
false
|
||||
},
|
||||
107 => {
|
||||
// PostBuildLogLine: 1 text field
|
||||
if let Some(line) = fields.first().and_then(|f| f.as_str()) {
|
||||
state.build_logs.push(format!("[post-build] {line}"));
|
||||
return true;
|
||||
}
|
||||
false
|
||||
},
|
||||
108 => {
|
||||
// FetchStatus: 1 text field
|
||||
if let Some(status) = fields.first().and_then(|f| f.as_str()) {
|
||||
debug!("Fetch status: {}", status);
|
||||
// Fetch status is informational
|
||||
}
|
||||
false
|
||||
},
|
||||
_ => {
|
||||
debug!("Unknown result type: {}", result_type);
|
||||
false
|
||||
105 => {
|
||||
// Build completed, fields contain output path
|
||||
complete_build(state, id)
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get build time estimate from cache
|
||||
fn get_build_estimate(
|
||||
state: &State,
|
||||
derivation_name: &str,
|
||||
host: &Host,
|
||||
) -> Option<u64> {
|
||||
// Use pname if available, otherwise derivation name
|
||||
let lookup_name = derivation_name.to_string();
|
||||
let host_str = host.name();
|
||||
|
||||
BuildReportCache::calculate_median(
|
||||
state
|
||||
.build_cache
|
||||
.get(&(host_str.to_string(), lookup_name))?
|
||||
.as_slice(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Record completed build for future predictions
|
||||
fn record_build_completion(
|
||||
state: &mut State,
|
||||
derivation_name: String,
|
||||
platform: Option<String>,
|
||||
start: f64,
|
||||
end: f64,
|
||||
host: &Host,
|
||||
) {
|
||||
let duration_secs = end - start;
|
||||
let completed_at = std::time::SystemTime::now();
|
||||
|
||||
let report = BuildReport {
|
||||
derivation_name: derivation_name.clone(),
|
||||
platform: platform.unwrap_or_default(),
|
||||
duration_secs,
|
||||
completed_at,
|
||||
host: host.name().to_string(),
|
||||
success: true,
|
||||
};
|
||||
|
||||
// Store in state for later CSV persistence
|
||||
let key = (host.name().to_string(), derivation_name);
|
||||
state.build_cache.entry(key).or_default().push(report);
|
||||
}
|
||||
|
||||
fn handle_build_start(
|
||||
state: &mut State,
|
||||
id: Id,
|
||||
|
|
@ -478,16 +298,13 @@ fn handle_build_start(
|
|||
if let Some(drv_path) = drv_path {
|
||||
debug!("Extracted derivation path: {}", drv_path);
|
||||
if let Some(drv) = Derivation::parse(&drv_path) {
|
||||
let drv_id = state.get_or_create_derivation_id(drv.clone());
|
||||
let drv_id = state.get_or_create_derivation_id(drv);
|
||||
let host = extract_host(text);
|
||||
|
||||
// Get build time estimate from cache
|
||||
let estimate = get_build_estimate(state, &drv.name, &host);
|
||||
|
||||
let build_info = BuildInfo {
|
||||
start: now,
|
||||
host,
|
||||
estimate,
|
||||
estimate: None,
|
||||
activity_id: Some(id),
|
||||
};
|
||||
|
||||
|
|
@ -506,59 +323,74 @@ fn handle_build_start(
|
|||
);
|
||||
|
||||
// Mark as forest root if no parent
|
||||
// Only add to forest roots if no parent
|
||||
if parent_id.is_none() && !state.forest_roots.contains(&drv_id) {
|
||||
state.forest_roots.push(drv_id);
|
||||
}
|
||||
|
||||
// Store activity -> derivation mapping
|
||||
// Phase will be extracted from log messages
|
||||
return true;
|
||||
}
|
||||
debug!("Failed to parse derivation from path: {}", drv_path);
|
||||
} else {
|
||||
debug!(
|
||||
"No derivation path in fields for Build activity {} - this should not \
|
||||
happen",
|
||||
"No derivation path found - creating placeholder for activity {}",
|
||||
id
|
||||
);
|
||||
// For shell/develop commands, nix doesn't report specific derivation paths
|
||||
// Create a placeholder derivation to track that builds are happening
|
||||
use std::path::PathBuf;
|
||||
|
||||
let placeholder_name = format!("building-{}", id);
|
||||
let placeholder_path = format!("/nix/store/placeholder-{}.drv", id);
|
||||
|
||||
let placeholder_drv = Derivation {
|
||||
path: PathBuf::from(placeholder_path),
|
||||
name: placeholder_name,
|
||||
};
|
||||
|
||||
let drv_id = state.get_or_create_derivation_id(placeholder_drv);
|
||||
let host = extract_host(text);
|
||||
|
||||
let build_info = BuildInfo {
|
||||
start: now,
|
||||
host,
|
||||
estimate: None,
|
||||
activity_id: Some(id),
|
||||
};
|
||||
|
||||
debug!(
|
||||
"Setting placeholder derivation {} to Building status",
|
||||
drv_id
|
||||
);
|
||||
state.update_build_status(drv_id, BuildStatus::Building(build_info));
|
||||
|
||||
// Mark as forest root if no parent
|
||||
if parent_id.is_none() && !state.forest_roots.contains(&drv_id) {
|
||||
state.forest_roots.push(drv_id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn handle_build_stop(state: &mut State, id: Id, now: f64) -> bool {
|
||||
// Check if we have success tokens to consume
|
||||
if state.success_tokens > 0 {
|
||||
// Find the derivation associated with this activity
|
||||
for (drv_id, info) in state.derivation_infos.clone().iter() {
|
||||
if let BuildStatus::Building(build_info) = &info.build_status {
|
||||
if build_info.activity_id == Some(id) {
|
||||
// Consume a success token and mark build as complete
|
||||
state.success_tokens = state.success_tokens.saturating_sub(1);
|
||||
state.update_build_status(*drv_id, BuildStatus::Built {
|
||||
info: build_info.clone(),
|
||||
end: now,
|
||||
});
|
||||
|
||||
// Record build completion for future predictions
|
||||
record_build_completion(
|
||||
state,
|
||||
info.name.name.clone(),
|
||||
info.platform.clone(),
|
||||
build_info.start,
|
||||
now,
|
||||
&build_info.host,
|
||||
);
|
||||
|
||||
debug!(
|
||||
"Build completed for derivation {} (success_tokens: {})",
|
||||
drv_id, state.success_tokens
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
fn handle_build_stop(state: &mut State, id: Id, _now: f64) -> bool {
|
||||
// Find the derivation associated with this activity
|
||||
for (drv_id, info) in &state.derivation_infos {
|
||||
match &info.build_status {
|
||||
BuildStatus::Building(build_info)
|
||||
if build_info.activity_id == Some(id) =>
|
||||
{
|
||||
// Build was stopped but not marked as completed
|
||||
// It might be cancelled
|
||||
debug!("Build stopped for derivation {}", drv_id);
|
||||
return false;
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
// No success tokens - build was stopped without completion signal
|
||||
debug!("Build stopped for activity {} without success token", id);
|
||||
false
|
||||
}
|
||||
|
||||
|
|
@ -645,111 +477,45 @@ fn handle_substitute_stop(state: &mut State, id: Id, now: f64) -> bool {
|
|||
false
|
||||
}
|
||||
|
||||
fn handle_file_transfer_start(
|
||||
_state: &mut State,
|
||||
id: Id,
|
||||
_text: &str,
|
||||
fields: &[serde_json::Value],
|
||||
_now: f64,
|
||||
) -> bool {
|
||||
// FileTransfer expects 1 text field: URL or description
|
||||
if fields.is_empty() {
|
||||
debug!("FileTransfer activity {} has no fields", id);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Just track the activity, actual progress comes via Result messages
|
||||
true
|
||||
}
|
||||
|
||||
fn handle_copy_path_start(
|
||||
fn handle_transfer_start(
|
||||
state: &mut State,
|
||||
id: Id,
|
||||
_text: &str,
|
||||
text: &str,
|
||||
fields: &[serde_json::Value],
|
||||
now: f64,
|
||||
is_copy: bool,
|
||||
) -> bool {
|
||||
// CopyPath expects 3 text fields: path, from, to
|
||||
if fields.len() < 3 {
|
||||
debug!("CopyPath activity {} has insufficient fields", id);
|
||||
return false;
|
||||
}
|
||||
let path_str = if fields.is_empty() {
|
||||
extract_store_path(text)
|
||||
} else {
|
||||
fields[0].as_str().map(std::string::ToString::to_string)
|
||||
};
|
||||
|
||||
let path_str = fields[0].as_str();
|
||||
let _from_host = fields[1].as_str().map(|s| {
|
||||
if s.is_empty() || s == "localhost" {
|
||||
Host::Localhost
|
||||
} else {
|
||||
Host::Remote(s.to_string())
|
||||
}
|
||||
});
|
||||
let to_host = fields[2].as_str().map(|s| {
|
||||
if s.is_empty() || s == "localhost" {
|
||||
Host::Localhost
|
||||
} else {
|
||||
Host::Remote(s.to_string())
|
||||
}
|
||||
});
|
||||
|
||||
if let (Some(path_str), Some(to)) = (path_str, to_host) {
|
||||
if let Some(path) = StorePath::parse(path_str) {
|
||||
if let Some(path_str) = path_str {
|
||||
if let Some(path) = StorePath::parse(&path_str) {
|
||||
let path_id = state.get_or_create_store_path_id(path);
|
||||
let host = extract_host(text);
|
||||
|
||||
let transfer = TransferInfo {
|
||||
start: now,
|
||||
host: to, // destination host
|
||||
activity_id: id,
|
||||
start: now,
|
||||
host,
|
||||
activity_id: id,
|
||||
bytes_transferred: 0,
|
||||
total_bytes: None,
|
||||
total_bytes: None,
|
||||
};
|
||||
|
||||
// CopyPath is an upload from 'from' to 'to'
|
||||
state.full_summary.running_uploads.insert(path_id, transfer);
|
||||
if is_copy {
|
||||
state.full_summary.running_uploads.insert(path_id, transfer);
|
||||
} else {
|
||||
state
|
||||
.full_summary
|
||||
.running_downloads
|
||||
.insert(path_id, transfer);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn handle_query_path_info_start(
|
||||
_state: &mut State,
|
||||
id: Id,
|
||||
_text: &str,
|
||||
fields: &[serde_json::Value],
|
||||
_now: f64,
|
||||
) -> bool {
|
||||
// QueryPathInfo expects 2 text fields: path, host
|
||||
if fields.len() < 2 {
|
||||
debug!("QueryPathInfo activity {} has insufficient fields", id);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Just track the activity
|
||||
true
|
||||
}
|
||||
|
||||
fn handle_post_build_hook_start(
|
||||
_state: &mut State,
|
||||
id: Id,
|
||||
_text: &str,
|
||||
fields: &[serde_json::Value],
|
||||
_now: f64,
|
||||
) -> bool {
|
||||
// PostBuildHook expects 1 text field: derivation path
|
||||
if fields.is_empty() {
|
||||
debug!("PostBuildHook activity {} has no fields", id);
|
||||
return false;
|
||||
}
|
||||
|
||||
let drv_path = fields[0].as_str();
|
||||
if let Some(drv_path) = drv_path {
|
||||
if let Some(_drv) = Derivation::parse(drv_path) {
|
||||
// Just track that the hook is running
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
|
|
@ -798,6 +564,54 @@ fn handle_transfer_stop(state: &mut State, id: Id, now: f64) -> bool {
|
|||
false
|
||||
}
|
||||
|
||||
fn update_transfer_progress(
|
||||
state: &mut State,
|
||||
id: Id,
|
||||
fields: &[serde_json::Value],
|
||||
) {
|
||||
if fields.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let bytes_transferred = fields[0].as_u64().unwrap_or(0);
|
||||
let total_bytes = fields[1].as_u64();
|
||||
|
||||
// Update running downloads
|
||||
for transfer_info in state.full_summary.running_downloads.values_mut() {
|
||||
if transfer_info.activity_id == id {
|
||||
transfer_info.bytes_transferred = bytes_transferred;
|
||||
transfer_info.total_bytes = total_bytes;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update running uploads
|
||||
for transfer_info in state.full_summary.running_uploads.values_mut() {
|
||||
if transfer_info.activity_id == id {
|
||||
transfer_info.bytes_transferred = bytes_transferred;
|
||||
transfer_info.total_bytes = total_bytes;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn complete_build(state: &mut State, id: Id) -> bool {
|
||||
// Find the derivation that just completed
|
||||
for (drv_id, info) in &state.derivation_infos.clone() {
|
||||
if let BuildStatus::Building(build_info) = &info.build_status {
|
||||
if build_info.activity_id == Some(id) {
|
||||
let end = current_time();
|
||||
state.update_build_status(*drv_id, BuildStatus::Built {
|
||||
info: build_info.clone(),
|
||||
end,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn extract_derivation_path(text: &str) -> Option<String> {
|
||||
// Look for .drv paths in the text
|
||||
if let Some(start) = text.find("/nix/store/") {
|
||||
|
|
@ -1072,3 +886,18 @@ pub fn finish_state(state: &mut State) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse output name string to `OutputName` enum
|
||||
fn parse_output_name(s: &str) -> Option<OutputName> {
|
||||
match s {
|
||||
"out" => Some(OutputName::Out),
|
||||
"doc" => Some(OutputName::Doc),
|
||||
"dev" => Some(OutputName::Dev),
|
||||
"bin" => Some(OutputName::Bin),
|
||||
"info" => Some(OutputName::Info),
|
||||
"lib" => Some(OutputName::Lib),
|
||||
"man" => Some(OutputName::Man),
|
||||
"dist" => Some(OutputName::Dist),
|
||||
other => Some(OutputName::Other(other.to_string())),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue