diff --git a/Cargo.lock b/Cargo.lock index 81a43f1..9595a3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -108,9 +108,9 @@ checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "clap" -version = "4.5.51" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" dependencies = [ "clap_builder", "clap_derive", @@ -118,9 +118,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" dependencies = [ "anstream", "anstyle", @@ -130,9 +130,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" dependencies = [ "heck", "proc-macro2", @@ -229,14 +229,14 @@ dependencies = [ [[package]] name = "csv" -version = "1.4.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" dependencies = [ "csv-core", "itoa", "ryu", - "serde_core", + "serde", ] [[package]] @@ -386,12 +386,6 @@ dependencies = [ "foldhash", ] -[[package]] -name = "hashbrown" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" - [[package]] name = "heck" version = "0.5.0" @@ -412,12 +406,12 @@ checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" [[package]] name = "indexmap" -version = "2.12.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown", "serde", "serde_core", ] @@ -514,7 +508,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.5", + "hashbrown", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 00288d0..f21c9bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,14 +11,14 @@ rust-version = "1.85" [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" +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"] } diff --git a/LICENSE b/LICENSE index de0a651..baa49b9 100644 --- a/LICENSE +++ b/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. diff --git a/cognos/src/lib.rs b/cognos/src/lib.rs index a6443ca..11bac2b 100644 --- a/cognos/src/lib.rs +++ b/cognos/src/lib.rs @@ -11,28 +11,16 @@ 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, -}; +pub use state::{BuildInfo, BuildStatus, Dependencies, Derivation, Host, OutputName, State, ProgressState}; /// Process a list of actions and return the resulting state -#[must_use] -pub fn process_actions(actions: Vec) -> State { +#[must_use] pub fn process_actions(actions: Vec) -> State { let mut state = State { - progress: ProgressState::JustStarted, - derivations: HashMap::new(), - builds: HashMap::new(), - dependencies: Dependencies { - deps: HashMap::new(), - }, - store_paths: HashMap::new(), + 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 { diff --git a/cognos/src/state.rs b/cognos/src/state.rs index c72db70..a4739c2 100644 --- a/cognos/src/state.rs +++ b/cognos/src/state.rs @@ -97,22 +97,18 @@ pub struct Dependencies { // #[derive(Default)] pub struct State { - pub progress: ProgressState, - pub derivations: HashMap, - pub builds: HashMap, - pub dependencies: Dependencies, - pub store_paths: HashMap, + pub progress: ProgressState, + pub derivations: HashMap, + pub builds: HashMap, + pub dependencies: Dependencies, + pub store_paths: HashMap, pub dependency_states: HashMap, } impl State { pub fn imbibe(&mut self, action: Actions) { match action { - Actions::Start { - id, - activity: _activity, - .. - } => { + Actions::Start { id, activity: _activity, .. } => { let derivation = Derivation { store_path: PathBuf::from("/nix/store/placeholder"), }; @@ -122,11 +118,11 @@ impl State { 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, + start: 0.0, // Placeholder, would need actual time + host: Host::Localhost, // Placeholder + estimate: None, activity_id: id, - state: BuildStatus::Running, + state: BuildStatus::Running, }; self.builds.insert(id, build_info.clone()); self.dependencies.deps.insert(id, build_info); @@ -156,4 +152,4 @@ impl State { }, } } -} +} \ No newline at end of file diff --git a/flake.lock b/flake.lock index 8a30674..4ab3227 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1765186076, - "narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=", + "lastModified": 1761672384, + "narHash": "sha256-o9KF3DJL7g7iYMZq9SWgfS1BFlNbsm6xplRjVlOCkXI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8", + "rev": "08dacfca559e1d7da38f3cf05f1f45ee9bfd213c", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 5550e67..18bd6fa 100644 --- a/flake.nix +++ b/flake.nix @@ -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, diff --git a/rom/src/cli.rs b/rom/src/cli.rs index 98ecaef..b0c5a70 100644 --- a/rom/src/cli.rs +++ b/rom/src/cli.rs @@ -254,8 +254,7 @@ 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( +#[must_use] pub fn parse_args_with_separator( args: &[String], ) -> (Vec, Vec) { if let Some(pos) = args.iter().position(|arg| arg == "--") { @@ -583,7 +582,9 @@ fn run_monitored_command( || !state.full_summary.planned_builds.is_empty(); if !silent { - if has_activity || state.progress_state != ProgressState::JustStarted { + if has_activity + || state.progress_state != ProgressState::JustStarted + { // Clear any previous timer display if last_timer_display.is_some() { display.clear_previous().ok(); diff --git a/rom/src/display.rs b/rom/src/display.rs index d8cb720..ac07178 100644 --- a/rom/src/display.rs +++ b/rom/src/display.rs @@ -14,8 +14,7 @@ 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 { +#[must_use] pub fn format_duration(secs: f64) -> String { if secs < 60.0 { format!("{secs:.0}s") } else if secs < 3600.0 { @@ -64,9 +63,9 @@ impl Default for DisplayConfig { } pub struct Display { - writer: W, - config: DisplayConfig, - last_lines: usize, + writer: W, + config: DisplayConfig, + last_lines: usize, } struct TreeNode { @@ -382,9 +381,9 @@ impl Display { || downloading > 0 || uploading > 0 { - lines.push(self.colored(&"═".repeat(60), Color::Blue).clone()); + lines.push(self.colored(&"═".repeat(60), Color::Blue).to_string()); lines.push(format!("{} Build Summary", self.colored("┃", Color::Blue))); - lines.push(self.colored(&"─".repeat(60), Color::Blue).clone()); + lines.push(self.colored(&"─".repeat(60), Color::Blue).to_string()); // Builds section if running + completed + failed > 0 { @@ -431,7 +430,7 @@ impl Display { self.format_duration(duration) )); - lines.push(self.colored(&"═".repeat(60), Color::Blue).clone()); + lines.push(self.colored(&"═".repeat(60), Color::Blue).to_string()); } lines @@ -861,6 +860,8 @@ impl Display { lines } + + fn build_active_forest( &self, state: &State, diff --git a/rom/src/monitor.rs b/rom/src/monitor.rs index 18e23b7..e147b06 100644 --- a/rom/src/monitor.rs +++ b/rom/src/monitor.rs @@ -1,25 +1,18 @@ //! Monitor module for orchestrating state updates and display rendering + use std::{ io::{BufRead, Write}, time::Duration, }; -use cognos::Host; - use crate::{ - display::{Display, DisplayConfig, LegendStyle, SummaryStyle}, + display::{Display, DisplayConfig}, error::{Result, RomError}, - state::{ - BuildStatus, - Derivation, - FailType, - State, - StorePath, - StorePathState, - }, + state::State, types::{Config, InputMode}, update, }; +use cognos::Host; /// Main monitor that processes nix output and displays progress pub struct Monitor { @@ -32,15 +25,15 @@ impl Monitor { /// Create a new monitor pub fn new(config: Config, writer: W) -> Result { 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 { @@ -181,21 +174,20 @@ impl Monitor { 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: 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 @@ -209,115 +201,14 @@ impl Monitor { } } - // 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::() { - // XXX: This is a PlanCopies message, we'll probably track this - // For now just acknowledge it, and let future work decide how - // we should go around doing it. - 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::().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) { @@ -328,35 +219,11 @@ impl Monitor { 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); } } @@ -404,33 +271,6 @@ fn extract_path_from_message(line: &str) -> Option { None } -/// Extract byte size from a message line (e.g., "downloaded 123 KiB") -fn extract_byte_size(line: &str) -> Option { - // 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::() { - 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::*; @@ -457,19 +297,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); - } } diff --git a/rom/src/state.rs b/rom/src/state.rs index cc47ee7..4ae014c 100644 --- a/rom/src/state.rs +++ b/rom/src/state.rs @@ -18,6 +18,10 @@ pub type DerivationId = usize; /// Unique identifier for activities pub type ActivityId = Id; + + + + /// Store path representation #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct StorePath { @@ -81,6 +85,9 @@ impl Derivation { } } + + + /// Transfer information (download/upload) #[derive(Debug, Clone)] pub struct TransferInfo { @@ -317,20 +324,6 @@ pub struct ActivityStatus { pub text: String, pub parent: Option, pub phase: Option, - pub progress: Option, -} - -/// 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 @@ -368,7 +361,6 @@ pub struct State { pub activities: HashMap, pub nix_errors: Vec, pub build_logs: Vec, - pub traces: Vec, pub build_platform: Option, pub evaluation_state: EvalInfo, next_store_path_id: StorePathId, @@ -398,7 +390,6 @@ impl State { activities: HashMap::new(), nix_errors: Vec::new(), build_logs: Vec::new(), - traces: Vec::new(), build_platform: None, evaluation_state: EvalInfo::default(), next_store_path_id: 0, @@ -673,30 +664,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 { - self - .derivation_infos - .keys() - .filter(|&&id| self.has_platform_mismatch(id)) - .copied() - .collect() - } } #[must_use] @@ -707,6 +674,8 @@ pub fn current_time() -> f64 { .as_secs_f64() } + + #[cfg(test)] mod tests { use super::*; diff --git a/rom/src/types.rs b/rom/src/types.rs index b355ec9..7358e86 100644 --- a/rom/src/types.rs +++ b/rom/src/types.rs @@ -23,8 +23,7 @@ pub enum SummaryStyle { } impl SummaryStyle { - #[must_use] - pub fn from_str(s: &str) -> Self { + #[must_use] pub fn from_str(s: &str) -> Self { match s.to_lowercase().as_str() { "concise" => Self::Concise, "table" => Self::Table, @@ -35,8 +34,7 @@ impl SummaryStyle { } impl DisplayFormat { - #[must_use] - pub fn from_str(s: &str) -> Self { + #[must_use] pub fn from_str(s: &str) -> Self { match s.to_lowercase().as_str() { "tree" => Self::Tree, "plain" => Self::Plain, diff --git a/rom/src/update.rs b/rom/src/update.rs index 0524dfb..98ec590 100644 --- a/rom/src/update.rs +++ b/rom/src/update.rs @@ -4,7 +4,6 @@ use cognos::{Actions, Activities, Host, Id, ProgressState, Verbosity}; use tracing::{debug, trace}; use crate::state::{ - ActivityProgress, ActivityStatus, BuildFail, BuildInfo, @@ -87,29 +86,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 - 102 | 103 | 104 | 106 | 107 | 111 | 112 => { - // Realise, CopyPaths, Builds, 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 @@ -119,8 +110,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 @@ -159,19 +150,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 { @@ -185,7 +166,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 @@ -247,14 +228,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 }, @@ -264,118 +237,38 @@ 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, _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 + 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(); - let _total = fields[1].as_u64(); - // TODO: Track file linking progress - } - 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: {}", path_str); - // TODO: Track untrusted paths - } - 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(), - ) { + // 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(); - let _expected_count = fields[1].as_u64(); - // TODO: Track expected counts - } - 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); - // TODO: Track fetch status - } - false - }, - _ => { - debug!("Unknown result type: {}", result_type); - false + 105 => { + // Build completed, fields contain output path + complete_build(state, id) }, + _ => false, } } @@ -427,19 +320,55 @@ 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-{id}.drv"); + + 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 } @@ -545,111 +474,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 } @@ -698,6 +561,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 { // Look for .drv paths in the text if let Some(start) = text.find("/nix/store/") { @@ -972,3 +883,6 @@ pub fn finish_state(state: &mut State) { } } } + + +