From de877a01abb0a0d271e64716ef449b51aa81d1ab Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 31 Oct 2025 19:45:48 +0300 Subject: [PATCH 01/10] chore: bump dependencies Signed-off-by: NotAShelf Change-Id: Id9632ce60230f9ec44681fbff7e1dc996a6a6964 --- Cargo.lock | 32 +++++++++++++++++++------------- Cargo.toml | 6 +++--- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9595a3d..81a43f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -108,9 +108,9 @@ checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "clap" -version = "4.5.48" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" dependencies = [ "clap_builder", "clap_derive", @@ -118,9 +118,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.48" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" dependencies = [ "anstream", "anstyle", @@ -130,9 +130,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.47" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", @@ -229,14 +229,14 @@ dependencies = [ [[package]] name = "csv" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" dependencies = [ "csv-core", "itoa", "ryu", - "serde", + "serde_core", ] [[package]] @@ -386,6 +386,12 @@ 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" @@ -406,12 +412,12 @@ checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" [[package]] name = "indexmap" -version = "2.11.4" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.0", "serde", "serde_core", ] @@ -508,7 +514,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown", + "hashbrown 0.15.5", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f21c9bc..00288d0 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.48", features = ["derive"] } +clap = { version = "4.5.51", 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.11.4", features = ["serde"] } -csv = "1.3.1" +indexmap = { version = "2.12.0", features = ["serde"] } +csv = "1.4.0" thiserror = "2.0.17" tracing = "0.1.41" tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } From d652faacc2272c94b5fc23f3f910fb038172b12b Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 31 Oct 2025 22:29:00 +0300 Subject: [PATCH 02/10] meta: relicense under EUPL v1.2 Signed-off-by: NotAShelf Change-Id: Ibee59f6778452c7c82f0046a5c44f3306a6a6964 --- LICENSE | 482 ++++++++++++++++++++++++++------------------------------ 1 file changed, 221 insertions(+), 261 deletions(-) diff --git a/LICENSE b/LICENSE index baa49b9..de0a651 100644 --- a/LICENSE +++ b/LICENSE @@ -1,328 +1,288 @@ -Mozilla Public License, version 2.0 + 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. 1. Definitions - 1.1. “Contributor” - means each individual or legal entity that creates, contributes to the - creation of, or owns Covered Software. +In this Licence, the following terms have the following meaning: - 1.2. “Contributor Version” - means the combination of the Contributions of others (if any) used by a - Contributor and that particular Contributor’s Contribution. +- ‘The Licence’: this Licence. - 1.3. “Contribution” - means Covered Software of a particular Contributor. +- ‘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. - 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. +- ‘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. - 1.5. “Incompatible With Secondary Licenses” - means +- ‘The Work’: the Original Work or its Derivative Works. - a. that the initial Contributor has attached the notice described - in Exhibit B to the Covered Software; or +- ‘The Source Code’: the human-readable form of the Work which is the most + convenient for people to study and modify. - 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. +- ‘The Executable Code’: any code which has generally been compiled and which is + meant to be interpreted by a computer as a program. - 1.6. “Executable Form” - means any form of the work other than Source Code Form. +- ‘The Licensor’: the natural or legal person that distributes or communicates + the Work under the Licence. - 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. +- ‘Contributor(s)’: any natural or legal person who modifies the Work under the + Licence, or otherwise contributes to the creation of a Derivative Work. - 1.8. “License” - means this document. +- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of + the Work under the terms of the Licence. - 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. +- ‘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. - 1.10. “Modifications” - means any of the following: +2. Scope of the rights granted by the Licence - a. any file in Source Code Form that results from an addition to, - deletion from, or modification of the contents of Covered Software; or +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: - b. any new file in Source Code Form that contains any Covered Software. +- 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. - 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. +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. - 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. +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. - 1.13. “Source Code Form” - means the form of the work preferred for making modifications. +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. - 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. +3. Communication of the Source Code -2. License Grants and Conditions +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. - 2.1. Grants - Each Contributor hereby grants You a world-wide, royalty-free, - non-exclusive license: +4. Limitations on copyright - 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 +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. - 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. +5. Obligations of the Licensee - 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. +The grant of the rights mentioned above is subject to some restrictions and +obligations imposed on the Licensee. Those obligations are the following: - 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: +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. - a. for any code that a Contributor has removed from - Covered Software; or +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. - 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 +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. - c. under Patent Claims infringed by Covered Software in the - absence of its Contributions. +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. - 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). +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. - 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). +6. Chain of Authorship - 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. +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. - 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. +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. - 2.7. Conditions - Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the - licenses granted in Section 2.1. +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. -3. Responsibilities +7. Disclaimer of Warranty - 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. +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. - 3.2. Distribution of Executable Form - If You distribute Covered Software in Executable Form then: +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. - 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 +This disclaimer of warranty is an essential part of the Licence and a condition +for the grant of any rights to the Work. - 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. +8. Disclaimer of Liability - 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). +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. - 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. +9. Additional agreements - 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. +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. -4. Inability to Comply Due to Statute or Regulation +10. Acceptance of the Licence -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. +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. -5. Termination +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. - 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. +11. Information to the public - 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. +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. - 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. +12. Termination of the Licence -6. Disclaimer of Warranty +The Licence and the rights granted hereunder will terminate automatically upon +any breach by the Licensee of the terms of the Licence. -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. +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. -7. Limitation of Liability +13. Miscellaneous -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. +Without prejudice of Article 9 above, the Licence represents the complete +agreement between the Parties as to the Work. -8. Litigation +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. -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. +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. -9. Miscellaneous +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. -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. +14. Jurisdiction -10. Versions of the License +Without prejudice to specific agreement between parties, - 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. +- 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, - 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. +- 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. - 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). +15. Applicable Law - 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. +Without prejudice to specific agreement between parties, -Exhibit A - Source Code Form License Notice +- 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 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/. +- this licence shall be governed by Belgian law if the Licensor has no seat, + residence or registered office inside a European Union Member State. -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. +Appendix -You may add additional accurate notices of copyright ownership. +‘Compatible Licences’ according to Article 5 EUPL are: -Exhibit B - “Incompatible With Secondary Licenses” Notice +- 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. - This Source Code Form is “Incompatible With Secondary Licenses”, - as defined by the Mozilla Public License, v. 2.0. From 58a3d68d22a02383761df16f13655df110af072c Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 16 Dec 2025 14:20:32 +0300 Subject: [PATCH 03/10] cognos: format Signed-off-by: NotAShelf Change-Id: Ie85796e5a4eb173faacdb355bdd58ffd6a6a6964 --- cognos/src/lib.rs | 26 +++++++++++++++++++------- cognos/src/state.rs | 26 +++++++++++++++----------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/cognos/src/lib.rs b/cognos/src/lib.rs index 11bac2b..a6443ca 100644 --- a/cognos/src/lib.rs +++ b/cognos/src/lib.rs @@ -11,16 +11,28 @@ pub use aterm::{ parse_drv_file, }; pub use internal_json::{Actions, Activities, Id, Verbosity}; -pub use state::{BuildInfo, BuildStatus, Dependencies, Derivation, Host, OutputName, State, ProgressState}; +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) -> 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 a4739c2..c72db70 100644 --- a/cognos/src/state.rs +++ b/cognos/src/state.rs @@ -97,18 +97,22 @@ 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"), }; @@ -118,11 +122,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); @@ -152,4 +156,4 @@ impl State { }, } } -} \ No newline at end of file +} From 4a0db12ecbb58ca9422e852a64545c3a6e1d13c7 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 17 Dec 2025 10:58:40 +0300 Subject: [PATCH 04/10] cognos: track activity progress for various goals Signed-off-by: NotAShelf Change-Id: I96a285c1e6f25b7061c61a4013b386ae6a6a6964 --- cognos/src/internal_json.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cognos/src/internal_json.rs b/cognos/src/internal_json.rs index a8c247f..3198ffd 100644 --- a/cognos/src/internal_json.rs +++ b/cognos/src/internal_json.rs @@ -35,6 +35,19 @@ pub enum Verbosity { Vomit = 7, } +/// Activity progress tracking for downloads/uploads/builds +#[derive(Deserialize, 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, +} + pub type Id = u64; #[derive(Deserialize, Debug, Clone)] From a22848532c9d7dca9ead11c2009fc4f24dceb60f Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 17 Dec 2025 10:59:14 +0300 Subject: [PATCH 05/10] monitor: improve human-readable log parser Better byte size parsing, new message type detection, and improved error parsing. - Add byte size parsing for KiB/MiB/GiB/TiB/PiB units, most of which I don't think most users will hit. - Parse "checking outputs of" and "copying N paths" messages - Improve error parsing to distinguish hash mismatches, exit codes, timeouts - Extract and track byte sizes for downloads/uploads - Associate errors with specific derivations and mark as failed Signed-off-by: NotAShelf Change-Id: I8b4beafb812cfacb7aca8de10170d6186a6a6964 --- rom/src/monitor.rs | 259 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 236 insertions(+), 23 deletions(-) diff --git a/rom/src/monitor.rs b/rom/src/monitor.rs index e147b06..52b113b 100644 --- a/rom/src/monitor.rs +++ b/rom/src/monitor.rs @@ -1,18 +1,25 @@ //! Monitor module for orchestrating state updates and display rendering - use std::{ io::{BufRead, Write}, time::Duration, }; +use cognos::Host; + use crate::{ - display::{Display, DisplayConfig}, + display::{Display, DisplayConfig, LegendStyle, SummaryStyle}, error::{Result, RomError}, - state::State, + state::{ + BuildStatus, + Derivation, + FailType, + State, + StorePath, + StorePathState, + }, types::{Config, InputMode}, update, }; -use cognos::Host; /// Main monitor that processes nix output and displays progress pub struct Monitor { @@ -25,15 +32,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" => crate::display::LegendStyle::Compact, - "verbose" => crate::display::LegendStyle::Verbose, - _ => crate::display::LegendStyle::Table, + "compact" => LegendStyle::Compact, + "verbose" => LegendStyle::Verbose, + _ => LegendStyle::Table, }; let summary_style = match config.summary_style.to_lowercase().as_str() { - "table" => crate::display::SummaryStyle::Table, - "full" => crate::display::SummaryStyle::Full, - _ => crate::display::SummaryStyle::Concise, + "table" => SummaryStyle::Table, + "full" => SummaryStyle::Full, + _ => SummaryStyle::Concise, }; let display_config = DisplayConfig { @@ -174,20 +181,21 @@ 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: None, + total_bytes, }; if let Some(path_info) = self.state.get_store_path_info_mut(path_id) { path_info .states - .insert(crate::state::StorePathState::Downloading( - transfer.clone(), - )); + .insert(StorePathState::Downloading(transfer.clone())); } self @@ -201,14 +209,116 @@ 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(|t| t.start) + .unwrap_or(now); + + 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); - } - // Detect build completions - if line.starts_with("built") || line.contains("built '") { + // 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 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) { @@ -219,11 +329,35 @@ impl Monitor { let now = crate::state::current_time(); self.state.update_build_status( drv_id, - crate::state::BuildStatus::Built { + crate::state::BuildStatus::Failed { info: build_info.clone(), - end: now, + fail: crate::state::BuildFail { + at: now, + fail_type: fail_type.clone(), + }, }, ); + } + } + } + } + } + + 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); } } @@ -271,6 +405,57 @@ fn extract_path_from_message(line: &str) -> Option { None } +/// Parse byte size from human-readable format (e.g., "123 KiB", "4.5 MiB") +/// Supports: B, KiB, MiB, GiB, TiB, PiB +fn parse_byte_size(text: &str) -> Option { + let parts: Vec<&str> = text.split_whitespace().collect(); + if parts.len() < 2 { + return None; + } + + let value: f64 = parts[0].parse().ok()?; + let unit = parts[1]; + + 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, + _ => return None, + }; + + Some((value * multiplier as f64) as u64) +} + +/// 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::*; @@ -297,4 +482,32 @@ mod tests { let path = extract_path_from_message(line); assert!(path.is_some()); } + + #[test] + fn test_parse_byte_size() { + assert_eq!(parse_byte_size("123 B"), Some(123)); + assert_eq!(parse_byte_size("1 KiB"), Some(1024)); + assert_eq!(parse_byte_size("1 MiB"), Some(1024 * 1024)); + assert_eq!(parse_byte_size("1 GiB"), Some(1024 * 1024 * 1024)); + assert_eq!( + parse_byte_size("2.5 MiB"), + Some((2.5 * 1024.0 * 1024.0) as u64) + ); + assert_eq!(parse_byte_size("invalid"), None); + } + + #[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); + } } From c57ef456799936b299e0407cb8309c5e19f2a611 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 17 Dec 2025 11:54:53 +0300 Subject: [PATCH 06/10] state: improve progress fields, trace storage, platform mismatch detection Signed-off-by: NotAShelf Change-Id: I2a4ce79fef265cfa26df26a0b258dd746a6a6964 --- rom/src/state.rs | 49 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/rom/src/state.rs b/rom/src/state.rs index 4ae014c..cc47ee7 100644 --- a/rom/src/state.rs +++ b/rom/src/state.rs @@ -18,10 +18,6 @@ pub type DerivationId = usize; /// Unique identifier for activities pub type ActivityId = Id; - - - - /// Store path representation #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct StorePath { @@ -85,9 +81,6 @@ impl Derivation { } } - - - /// Transfer information (download/upload) #[derive(Debug, Clone)] pub struct TransferInfo { @@ -324,6 +317,20 @@ 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 @@ -361,6 +368,7 @@ 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, @@ -390,6 +398,7 @@ 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, @@ -664,6 +673,30 @@ 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] @@ -674,8 +707,6 @@ pub fn current_time() -> f64 { .as_secs_f64() } - - #[cfg(test)] mod tests { use super::*; From 48ee32f3d66544c18b842e04d01c07aaf95435c0 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 17 Dec 2025 11:56:11 +0300 Subject: [PATCH 07/10] update: better message processing; fix build errors Signed-off-by: NotAShelf Change-Id: I606a3b414a93a7d636400933fef5e6776a6a6964 --- rom/src/update.rs | 49 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/rom/src/update.rs b/rom/src/update.rs index 98ec590..c3076a2 100644 --- a/rom/src/update.rs +++ b/rom/src/update.rs @@ -4,6 +4,7 @@ use cognos::{Actions, Activities, Host, Id, ProgressState, Verbosity}; use tracing::{debug, trace}; use crate::state::{ + ActivityProgress, ActivityStatus, BuildFail, BuildInfo, @@ -86,6 +87,7 @@ fn handle_start( text: text.clone(), parent: parent_id, phase: None, + progress: None, }); let changed = match activity_u8 { @@ -228,6 +230,14 @@ 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 }, @@ -245,14 +255,14 @@ fn handle_result( 101 | 108 => { // FileTransfer or Substitute // Fields contain progress information - // XXX: Format: [bytes_transferred, total_bytes] + // Format: [bytes_transferred, total_bytes] if fields.len() >= 2 { update_transfer_progress(state, id, &fields); } false }, 104 => { - // Builds activity type - contains phase information + // Builds activity type - contains phase information or progress if !fields.is_empty() { if let Some(phase_str) = fields[0].as_str() { // Update the activity's phase field @@ -265,8 +275,36 @@ fn handle_result( false }, 105 => { - // Build completed, fields contain output path - complete_build(state, id) + // Progress update (done, expected, running, failed) + // OR Build completed (fields contain output path as string) + if fields.len() >= 4 { + // This is a progress update: [done, expected, running, failed] + 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 let Some(activity) = state.activities.get_mut(&id) { + activity.progress = Some(ActivityProgress { + done, + expected, + running, + failed, + }); + return true; + } + return false; + } + } + + if !fields.is_empty() && fields[0].is_string() { + // This is a build completion with output path + complete_build(state, id) + } else { + // Legacy: just mark build as complete + complete_build(state, id) + } }, _ => false, } @@ -883,6 +921,3 @@ pub fn finish_state(state: &mut State) { } } } - - - From 0eac066a043c6f88fec5d14f207a2fda6bff000f Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 17 Dec 2025 11:57:08 +0300 Subject: [PATCH 08/10] treewide: format; minor fixes Signed-off-by: NotAShelf Change-Id: Iad55f84850587e9a0862882fd03df5596a6a6964 --- rom/src/cli.rs | 7 +++---- rom/src/display.rs | 11 +++++------ rom/src/types.rs | 6 ++++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/rom/src/cli.rs b/rom/src/cli.rs index b0c5a70..98ecaef 100644 --- a/rom/src/cli.rs +++ b/rom/src/cli.rs @@ -254,7 +254,8 @@ 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 == "--") { @@ -582,9 +583,7 @@ 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 ac07178..6def649 100644 --- a/rom/src/display.rs +++ b/rom/src/display.rs @@ -14,7 +14,8 @@ 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 { @@ -63,9 +64,9 @@ impl Default for DisplayConfig { } pub struct Display { - writer: W, - config: DisplayConfig, - last_lines: usize, + writer: W, + config: DisplayConfig, + last_lines: usize, } struct TreeNode { @@ -860,8 +861,6 @@ impl Display { lines } - - fn build_active_forest( &self, state: &State, diff --git a/rom/src/types.rs b/rom/src/types.rs index 7358e86..b355ec9 100644 --- a/rom/src/types.rs +++ b/rom/src/types.rs @@ -23,7 +23,8 @@ 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, @@ -34,7 +35,8 @@ 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, From 503fcbf4e29c4d88df41e0516d5b25c2786810b3 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 17 Dec 2025 15:06:33 +0300 Subject: [PATCH 09/10] flake: update nixpkgs Signed-off-by: NotAShelf Change-Id: I0ab71afe6ecc1ba496b57cb80d8fb17b6a6a6964 --- flake.lock | 6 +++--- flake.nix | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 4ab3227..8a30674 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1761672384, - "narHash": "sha256-o9KF3DJL7g7iYMZq9SWgfS1BFlNbsm6xplRjVlOCkXI=", + "lastModified": 1765186076, + "narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "08dacfca559e1d7da38f3cf05f1f45ee9bfd213c", + "rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 18bd6fa..5550e67 100644 --- a/flake.nix +++ b/flake.nix @@ -1,7 +1,5 @@ { - description = "Rust Project Template"; - inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - + inputs.nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable"; outputs = { self, nixpkgs, From 287dec65c39e6e78313e89498588ebec16227483 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 17 Dec 2025 13:46:15 +0300 Subject: [PATCH 10/10] treewide: remove dead code; track more activity types Signed-off-by: NotAShelf Change-Id: I8ef141010291a30f28d1d3e8bb9567046a6a6964 --- cognos/src/internal_json.rs | 13 -- rom/src/display.rs | 6 +- rom/src/monitor.rs | 40 +--- rom/src/update.rs | 355 +++++++++++++++++++++--------------- 4 files changed, 207 insertions(+), 207 deletions(-) diff --git a/cognos/src/internal_json.rs b/cognos/src/internal_json.rs index 3198ffd..a8c247f 100644 --- a/cognos/src/internal_json.rs +++ b/cognos/src/internal_json.rs @@ -35,19 +35,6 @@ pub enum Verbosity { Vomit = 7, } -/// Activity progress tracking for downloads/uploads/builds -#[derive(Deserialize, 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, -} - pub type Id = u64; #[derive(Deserialize, Debug, Clone)] diff --git a/rom/src/display.rs b/rom/src/display.rs index 6def649..d8cb720 100644 --- a/rom/src/display.rs +++ b/rom/src/display.rs @@ -382,9 +382,9 @@ impl Display { || downloading > 0 || uploading > 0 { - lines.push(self.colored(&"═".repeat(60), Color::Blue).to_string()); + lines.push(self.colored(&"═".repeat(60), Color::Blue).clone()); lines.push(format!("{} Build Summary", self.colored("┃", Color::Blue))); - lines.push(self.colored(&"─".repeat(60), Color::Blue).to_string()); + lines.push(self.colored(&"─".repeat(60), Color::Blue).clone()); // Builds section if running + completed + failed > 0 { @@ -431,7 +431,7 @@ impl Display { self.format_duration(duration) )); - lines.push(self.colored(&"═".repeat(60), Color::Blue).to_string()); + lines.push(self.colored(&"═".repeat(60), Color::Blue).clone()); } lines diff --git a/rom/src/monitor.rs b/rom/src/monitor.rs index 52b113b..18e23b7 100644 --- a/rom/src/monitor.rs +++ b/rom/src/monitor.rs @@ -223,8 +223,7 @@ impl Monitor { .full_summary .running_downloads .get(&path_id) - .map(|t| t.start) - .unwrap_or(now); + .map_or(now, |t| t.start); let completed = crate::state::CompletedTransferInfo { start, @@ -405,30 +404,6 @@ fn extract_path_from_message(line: &str) -> Option { None } -/// Parse byte size from human-readable format (e.g., "123 KiB", "4.5 MiB") -/// Supports: B, KiB, MiB, GiB, TiB, PiB -fn parse_byte_size(text: &str) -> Option { - let parts: Vec<&str> = text.split_whitespace().collect(); - if parts.len() < 2 { - return None; - } - - let value: f64 = parts[0].parse().ok()?; - let unit = parts[1]; - - 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, - _ => return None, - }; - - Some((value * multiplier as f64) as u64) -} - /// 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. @@ -483,19 +458,6 @@ mod tests { assert!(path.is_some()); } - #[test] - fn test_parse_byte_size() { - assert_eq!(parse_byte_size("123 B"), Some(123)); - assert_eq!(parse_byte_size("1 KiB"), Some(1024)); - assert_eq!(parse_byte_size("1 MiB"), Some(1024 * 1024)); - assert_eq!(parse_byte_size("1 GiB"), Some(1024 * 1024 * 1024)); - assert_eq!( - parse_byte_size("2.5 MiB"), - Some((2.5 * 1024.0 * 1024.0) as u64) - ); - assert_eq!(parse_byte_size("invalid"), None); - } - #[test] fn test_extract_byte_size() { let line = "downloaded 123 KiB in 2 seconds"; diff --git a/rom/src/update.rs b/rom/src/update.rs index c3076a2..0524dfb 100644 --- a/rom/src/update.rs +++ b/rom/src/update.rs @@ -91,18 +91,25 @@ fn handle_start( }); let changed = match activity_u8 { - 104 | 105 => handle_build_start(state, id, parent_id, &text, &fields, now), /* Builds | Build */ + 105 => handle_build_start(state, id, parent_id, &text, &fields, now), /* Build */ 108 => handle_substitute_start(state, id, &text, &fields, now), /* Substitute */ - 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, + 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 + }, }; // Track parent-child relationships for dependency tree - if changed - && (activity_u8 == 104 || activity_u8 == 105) - && parent_id.is_some() - { + if changed && activity_u8 == 105 && parent_id.is_some() { let parent_act_id = parent_id.unwrap(); // Find parent and child derivation IDs @@ -112,8 +119,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={}, child={}", - parent_drv_id, child_drv_id + "Establishing parent-child relationship: parent={parent_drv_id}, \ + child={child_drv_id}" ); // Add child as a dependency of parent @@ -152,9 +159,19 @@ fn handle_stop(state: &mut State, id: Id, now: f64) -> bool { state.activities.remove(&id); match activity_status.activity { - 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 */ + 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 + }, _ => false, } } else { @@ -168,7 +185,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,38 +264,70 @@ fn handle_message(state: &mut State, level: Verbosity, msg: String) -> bool { fn handle_result( state: &mut State, id: Id, - activity: u8, + result_type: u8, fields: Vec, _now: f64, ) -> bool { - match activity { - 101 | 108 => { - // FileTransfer or Substitute - // Fields contain progress information - // Format: [bytes_transferred, total_bytes] + // 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 if fields.len() >= 2 { - update_transfer_progress(state, id, &fields); + 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; } false }, 104 => { - // Builds activity type - contains phase information or progress - 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.phase = Some(phase_str.to_string()); - return true; - } + // 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 update (done, expected, running, failed) - // OR Build completed (fields contain output path as string) + // Progress: 4 int fields (done, expected, running, failed) if fields.len() >= 4 { - // This is a progress update: [done, expected, running, failed] if let (Some(done), Some(expected), Some(running), Some(failed)) = ( fields[0].as_u64(), fields[1].as_u64(), @@ -294,19 +343,39 @@ fn handle_result( }); return true; } - return false; } } - - if !fields.is_empty() && fields[0].is_string() { - // This is a build completion with output path - complete_build(state, id) - } else { - // Legacy: just mark build as complete - complete_build(state, id) - } + 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 }, - _ => false, } } @@ -358,55 +427,19 @@ 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 found - creating placeholder for activity {}", + "No derivation path in fields for Build activity {} - this should not \ + happen", 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 } @@ -512,45 +545,111 @@ fn handle_substitute_stop(state: &mut State, id: Id, now: f64) -> bool { false } -fn handle_transfer_start( +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( state: &mut State, id: Id, - text: &str, + _text: &str, fields: &[serde_json::Value], now: f64, - is_copy: bool, ) -> bool { - let path_str = if fields.is_empty() { - extract_store_path(text) - } else { - fields[0].as_str().map(std::string::ToString::to_string) - }; + // CopyPath expects 3 text fields: path, from, to + if fields.len() < 3 { + debug!("CopyPath activity {} has insufficient fields", id); + return false; + } - if let Some(path_str) = path_str { - if let Some(path) = StorePath::parse(&path_str) { + 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) { let path_id = state.get_or_create_store_path_id(path); - let host = extract_host(text); let transfer = TransferInfo { - start: now, - host, - activity_id: id, + start: now, + host: to, // destination host + activity_id: id, bytes_transferred: 0, - total_bytes: None, + total_bytes: None, }; - if is_copy { - state.full_summary.running_uploads.insert(path_id, transfer); - } else { - state - .full_summary - .running_downloads - .insert(path_id, transfer); - } - + // CopyPath is an upload from 'from' to 'to' + state.full_summary.running_uploads.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 } @@ -599,54 +698,6 @@ 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/") {