a linter for your flake.lock file

feat: init

+759
+3
.envrc
··· 1 + if has nix; then 2 + use flake 3 + fi
+1
.gitignore
··· 1 + target
+135
Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "argh" 7 + version = "0.1.13" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "34ff18325c8a36b82f992e533ece1ec9f9a9db446bd1c14d4f936bac88fcd240" 10 + dependencies = [ 11 + "argh_derive", 12 + "argh_shared", 13 + "rust-fuzzy-search", 14 + ] 15 + 16 + [[package]] 17 + name = "argh_derive" 18 + version = "0.1.13" 19 + source = "registry+https://github.com/rust-lang/crates.io-index" 20 + checksum = "adb7b2b83a50d329d5d8ccc620f5c7064028828538bdf5646acd60dc1f767803" 21 + dependencies = [ 22 + "argh_shared", 23 + "proc-macro2", 24 + "quote", 25 + "syn", 26 + ] 27 + 28 + [[package]] 29 + name = "argh_shared" 30 + version = "0.1.13" 31 + source = "registry+https://github.com/rust-lang/crates.io-index" 32 + checksum = "a464143cc82dedcdc3928737445362466b7674b5db4e2eb8e869846d6d84f4f6" 33 + dependencies = [ 34 + "serde", 35 + ] 36 + 37 + [[package]] 38 + name = "itoa" 39 + version = "1.0.15" 40 + source = "registry+https://github.com/rust-lang/crates.io-index" 41 + checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 42 + 43 + [[package]] 44 + name = "locker" 45 + version = "0.0.1" 46 + dependencies = [ 47 + "argh", 48 + "serde", 49 + "serde_json", 50 + ] 51 + 52 + [[package]] 53 + name = "memchr" 54 + version = "2.7.5" 55 + source = "registry+https://github.com/rust-lang/crates.io-index" 56 + checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 57 + 58 + [[package]] 59 + name = "proc-macro2" 60 + version = "1.0.95" 61 + source = "registry+https://github.com/rust-lang/crates.io-index" 62 + checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 63 + dependencies = [ 64 + "unicode-ident", 65 + ] 66 + 67 + [[package]] 68 + name = "quote" 69 + version = "1.0.40" 70 + source = "registry+https://github.com/rust-lang/crates.io-index" 71 + checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 72 + dependencies = [ 73 + "proc-macro2", 74 + ] 75 + 76 + [[package]] 77 + name = "rust-fuzzy-search" 78 + version = "0.1.1" 79 + source = "registry+https://github.com/rust-lang/crates.io-index" 80 + checksum = "a157657054ffe556d8858504af8a672a054a6e0bd9e8ee531059100c0fa11bb2" 81 + 82 + [[package]] 83 + name = "ryu" 84 + version = "1.0.20" 85 + source = "registry+https://github.com/rust-lang/crates.io-index" 86 + checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 87 + 88 + [[package]] 89 + name = "serde" 90 + version = "1.0.219" 91 + source = "registry+https://github.com/rust-lang/crates.io-index" 92 + checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 93 + dependencies = [ 94 + "serde_derive", 95 + ] 96 + 97 + [[package]] 98 + name = "serde_derive" 99 + version = "1.0.219" 100 + source = "registry+https://github.com/rust-lang/crates.io-index" 101 + checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 102 + dependencies = [ 103 + "proc-macro2", 104 + "quote", 105 + "syn", 106 + ] 107 + 108 + [[package]] 109 + name = "serde_json" 110 + version = "1.0.141" 111 + source = "registry+https://github.com/rust-lang/crates.io-index" 112 + checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" 113 + dependencies = [ 114 + "itoa", 115 + "memchr", 116 + "ryu", 117 + "serde", 118 + ] 119 + 120 + [[package]] 121 + name = "syn" 122 + version = "2.0.104" 123 + source = "registry+https://github.com/rust-lang/crates.io-index" 124 + checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" 125 + dependencies = [ 126 + "proc-macro2", 127 + "quote", 128 + "unicode-ident", 129 + ] 130 + 131 + [[package]] 132 + name = "unicode-ident" 133 + version = "1.0.18" 134 + source = "registry+https://github.com/rust-lang/crates.io-index" 135 + checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+13
Cargo.toml
··· 1 + [package] 2 + name = "locker" 3 + version = "0.0.1" 4 + license = "EUPL-1.2" 5 + description = "linting for your flake.lock" 6 + homepage = "https://github.com/isabelroses/locker" 7 + authors = ["isabel roses"] 8 + edition = "2024" 9 + 10 + [dependencies] 11 + argh = "0.1.13" 12 + serde = { version = "1.0.219", features = ["derive"] } 13 + serde_json = "1.0.141"
+287
LICENSE
··· 1 + EUROPEAN UNION PUBLIC LICENCE v. 1.2 2 + EUPL © the European Union 2007, 2016 3 + 4 + This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined 5 + below) which is provided under the terms of this Licence. Any use of the Work, 6 + other than as authorised under this Licence is prohibited (to the extent such 7 + use is covered by a right of the copyright holder of the Work). 8 + 9 + The Work is provided under the terms of this Licence when the Licensor (as 10 + defined below) has placed the following notice immediately following the 11 + copyright notice for the Work: 12 + 13 + Licensed under the EUPL 14 + 15 + or has expressed by any other means his willingness to license under the EUPL. 16 + 17 + 1. Definitions 18 + 19 + In this Licence, the following terms have the following meaning: 20 + 21 + - ‘The Licence’: this Licence. 22 + 23 + - ‘The Original Work’: the work or software distributed or communicated by the 24 + Licensor under this Licence, available as Source Code and also as Executable 25 + Code as the case may be. 26 + 27 + - ‘Derivative Works’: the works or software that could be created by the 28 + Licensee, based upon the Original Work or modifications thereof. This Licence 29 + does not define the extent of modification or dependence on the Original Work 30 + required in order to classify a work as a Derivative Work; this extent is 31 + determined by copyright law applicable in the country mentioned in Article 15. 32 + 33 + - ‘The Work’: the Original Work or its Derivative Works. 34 + 35 + - ‘The Source Code’: the human-readable form of the Work which is the most 36 + convenient for people to study and modify. 37 + 38 + - ‘The Executable Code’: any code which has generally been compiled and which is 39 + meant to be interpreted by a computer as a program. 40 + 41 + - ‘The Licensor’: the natural or legal person that distributes or communicates 42 + the Work under the Licence. 43 + 44 + - ‘Contributor(s)’: any natural or legal person who modifies the Work under the 45 + Licence, or otherwise contributes to the creation of a Derivative Work. 46 + 47 + - ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of 48 + the Work under the terms of the Licence. 49 + 50 + - ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, 51 + renting, distributing, communicating, transmitting, or otherwise making 52 + available, online or offline, copies of the Work or providing access to its 53 + essential functionalities at the disposal of any other natural or legal 54 + person. 55 + 56 + 2. Scope of the rights granted by the Licence 57 + 58 + The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, 59 + sublicensable licence to do the following, for the duration of copyright vested 60 + in the Original Work: 61 + 62 + - use the Work in any circumstance and for all usage, 63 + - reproduce the Work, 64 + - modify the Work, and make Derivative Works based upon the Work, 65 + - communicate to the public, including the right to make available or display 66 + the Work or copies thereof to the public and perform publicly, as the case may 67 + be, the Work, 68 + - distribute the Work or copies thereof, 69 + - lend and rent the Work or copies thereof, 70 + - sublicense rights in the Work or copies thereof. 71 + 72 + Those rights can be exercised on any media, supports and formats, whether now 73 + known or later invented, as far as the applicable law permits so. 74 + 75 + In the countries where moral rights apply, the Licensor waives his right to 76 + exercise his moral right to the extent allowed by law in order to make effective 77 + the licence of the economic rights here above listed. 78 + 79 + The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to 80 + any patents held by the Licensor, to the extent necessary to make use of the 81 + rights granted on the Work under this Licence. 82 + 83 + 3. Communication of the Source Code 84 + 85 + The Licensor may provide the Work either in its Source Code form, or as 86 + Executable Code. If the Work is provided as Executable Code, the Licensor 87 + provides in addition a machine-readable copy of the Source Code of the Work 88 + along with each copy of the Work that the Licensor distributes or indicates, in 89 + a notice following the copyright notice attached to the Work, a repository where 90 + the Source Code is easily and freely accessible for as long as the Licensor 91 + continues to distribute or communicate the Work. 92 + 93 + 4. Limitations on copyright 94 + 95 + Nothing in this Licence is intended to deprive the Licensee of the benefits from 96 + any exception or limitation to the exclusive rights of the rights owners in the 97 + Work, of the exhaustion of those rights or of other applicable limitations 98 + thereto. 99 + 100 + 5. Obligations of the Licensee 101 + 102 + The grant of the rights mentioned above is subject to some restrictions and 103 + obligations imposed on the Licensee. Those obligations are the following: 104 + 105 + Attribution right: The Licensee shall keep intact all copyright, patent or 106 + trademarks notices and all notices that refer to the Licence and to the 107 + disclaimer of warranties. The Licensee must include a copy of such notices and a 108 + copy of the Licence with every copy of the Work he/she distributes or 109 + communicates. The Licensee must cause any Derivative Work to carry prominent 110 + notices stating that the Work has been modified and the date of modification. 111 + 112 + Copyleft clause: If the Licensee distributes or communicates copies of the 113 + Original Works or Derivative Works, this Distribution or Communication will be 114 + done under the terms of this Licence or of a later version of this Licence 115 + unless the Original Work is expressly distributed only under this version of the 116 + Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee 117 + (becoming Licensor) cannot offer or impose any additional terms or conditions on 118 + the Work or Derivative Work that alter or restrict the terms of the Licence. 119 + 120 + Compatibility clause: If the Licensee Distributes or Communicates Derivative 121 + Works or copies thereof based upon both the Work and another work licensed under 122 + a Compatible Licence, this Distribution or Communication can be done under the 123 + terms of this Compatible Licence. For the sake of this clause, ‘Compatible 124 + Licence’ refers to the licences listed in the appendix attached to this Licence. 125 + Should the Licensee's obligations under the Compatible Licence conflict with 126 + his/her obligations under this Licence, the obligations of the Compatible 127 + Licence shall prevail. 128 + 129 + Provision of Source Code: When distributing or communicating copies of the Work, 130 + the Licensee will provide a machine-readable copy of the Source Code or indicate 131 + a repository where this Source will be easily and freely available for as long 132 + as the Licensee continues to distribute or communicate the Work. 133 + 134 + Legal Protection: This Licence does not grant permission to use the trade names, 135 + trademarks, service marks, or names of the Licensor, except as required for 136 + reasonable and customary use in describing the origin of the Work and 137 + reproducing the content of the copyright notice. 138 + 139 + 6. Chain of Authorship 140 + 141 + The original Licensor warrants that the copyright in the Original Work granted 142 + hereunder is owned by him/her or licensed to him/her and that he/she has the 143 + power and authority to grant the Licence. 144 + 145 + Each Contributor warrants that the copyright in the modifications he/she brings 146 + to the Work are owned by him/her or licensed to him/her and that he/she has the 147 + power and authority to grant the Licence. 148 + 149 + Each time You accept the Licence, the original Licensor and subsequent 150 + Contributors grant You a licence to their contributions to the Work, under the 151 + terms of this Licence. 152 + 153 + 7. Disclaimer of Warranty 154 + 155 + The Work is a work in progress, which is continuously improved by numerous 156 + Contributors. It is not a finished work and may therefore contain defects or 157 + ‘bugs’ inherent to this type of development. 158 + 159 + For the above reason, the Work is provided under the Licence on an ‘as is’ basis 160 + and without warranties of any kind concerning the Work, including without 161 + limitation merchantability, fitness for a particular purpose, absence of defects 162 + or errors, accuracy, non-infringement of intellectual property rights other than 163 + copyright as stated in Article 6 of this Licence. 164 + 165 + This disclaimer of warranty is an essential part of the Licence and a condition 166 + for the grant of any rights to the Work. 167 + 168 + 8. Disclaimer of Liability 169 + 170 + Except in the cases of wilful misconduct or damages directly caused to natural 171 + persons, the Licensor will in no event be liable for any direct or indirect, 172 + material or moral, damages of any kind, arising out of the Licence or of the use 173 + of the Work, including without limitation, damages for loss of goodwill, work 174 + stoppage, computer failure or malfunction, loss of data or any commercial 175 + damage, even if the Licensor has been advised of the possibility of such damage. 176 + However, the Licensor will be liable under statutory product liability laws as 177 + far such laws apply to the Work. 178 + 179 + 9. Additional agreements 180 + 181 + While distributing the Work, You may choose to conclude an additional agreement, 182 + defining obligations or services consistent with this Licence. However, if 183 + accepting obligations, You may act only on your own behalf and on your sole 184 + responsibility, not on behalf of the original Licensor or any other Contributor, 185 + and only if You agree to indemnify, defend, and hold each Contributor harmless 186 + for any liability incurred by, or claims asserted against such Contributor by 187 + the fact You have accepted any warranty or additional liability. 188 + 189 + 10. Acceptance of the Licence 190 + 191 + The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ 192 + placed under the bottom of a window displaying the text of this Licence or by 193 + affirming consent in any other similar way, in accordance with the rules of 194 + applicable law. Clicking on that icon indicates your clear and irrevocable 195 + acceptance of this Licence and all of its terms and conditions. 196 + 197 + Similarly, you irrevocably accept this Licence and all of its terms and 198 + conditions by exercising any rights granted to You by Article 2 of this Licence, 199 + such as the use of the Work, the creation by You of a Derivative Work or the 200 + Distribution or Communication by You of the Work or copies thereof. 201 + 202 + 11. Information to the public 203 + 204 + In case of any Distribution or Communication of the Work by means of electronic 205 + communication by You (for example, by offering to download the Work from a 206 + remote location) the distribution channel or media (for example, a website) must 207 + at least provide to the public the information requested by the applicable law 208 + regarding the Licensor, the Licence and the way it may be accessible, concluded, 209 + stored and reproduced by the Licensee. 210 + 211 + 12. Termination of the Licence 212 + 213 + The Licence and the rights granted hereunder will terminate automatically upon 214 + any breach by the Licensee of the terms of the Licence. 215 + 216 + Such a termination will not terminate the licences of any person who has 217 + received the Work from the Licensee under the Licence, provided such persons 218 + remain in full compliance with the Licence. 219 + 220 + 13. Miscellaneous 221 + 222 + Without prejudice of Article 9 above, the Licence represents the complete 223 + agreement between the Parties as to the Work. 224 + 225 + If any provision of the Licence is invalid or unenforceable under applicable 226 + law, this will not affect the validity or enforceability of the Licence as a 227 + whole. Such provision will be construed or reformed so as necessary to make it 228 + valid and enforceable. 229 + 230 + The European Commission may publish other linguistic versions or new versions of 231 + this Licence or updated versions of the Appendix, so far this is required and 232 + reasonable, without reducing the scope of the rights granted by the Licence. New 233 + versions of the Licence will be published with a unique version number. 234 + 235 + All linguistic versions of this Licence, approved by the European Commission, 236 + have identical value. Parties can take advantage of the linguistic version of 237 + their choice. 238 + 239 + 14. Jurisdiction 240 + 241 + Without prejudice to specific agreement between parties, 242 + 243 + - any litigation resulting from the interpretation of this License, arising 244 + between the European Union institutions, bodies, offices or agencies, as a 245 + Licensor, and any Licensee, will be subject to the jurisdiction of the Court 246 + of Justice of the European Union, as laid down in article 272 of the Treaty on 247 + the Functioning of the European Union, 248 + 249 + - any litigation arising between other parties and resulting from the 250 + interpretation of this License, will be subject to the exclusive jurisdiction 251 + of the competent court where the Licensor resides or conducts its primary 252 + business. 253 + 254 + 15. Applicable Law 255 + 256 + Without prejudice to specific agreement between parties, 257 + 258 + - this Licence shall be governed by the law of the European Union Member State 259 + where the Licensor has his seat, resides or has his registered office, 260 + 261 + - this licence shall be governed by Belgian law if the Licensor has no seat, 262 + residence or registered office inside a European Union Member State. 263 + 264 + Appendix 265 + 266 + ‘Compatible Licences’ according to Article 5 EUPL are: 267 + 268 + - GNU General Public License (GPL) v. 2, v. 3 269 + - GNU Affero General Public License (AGPL) v. 3 270 + - Open Software License (OSL) v. 2.1, v. 3.0 271 + - Eclipse Public License (EPL) v. 1.0 272 + - CeCILL v. 2.0, v. 2.1 273 + - Mozilla Public Licence (MPL) v. 2 274 + - GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 275 + - Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for 276 + works other than software 277 + - European Union Public Licence (EUPL) v. 1.1, v. 1.2 278 + - Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong 279 + Reciprocity (LiLiQ-R+). 280 + 281 + The European Commission may update this Appendix to later versions of the above 282 + licences without producing a new version of the EUPL, as long as they provide 283 + the rights granted in Article 2 of this Licence and protect the covered Source 284 + Code from exclusive appropriation. 285 + 286 + All other changes or additions to this Appendix require the production of a new 287 + EUPL version.
+5
README.md
··· 1 + ## Locker Lint 2 + 3 + Locker lint is a tool designed to lint your flake.lock file to find duplicate entries by their flake uri. 4 + 5 +
+28
default.nix
··· 1 + { lib, rustPlatform }: 2 + let 3 + toml = (lib.importTOML ./Cargo.toml).package; 4 + in 5 + rustPlatform.buildRustPackage { 6 + pname = "locker-rust"; 7 + inherit (toml) version; 8 + 9 + src = lib.fileset.toSource { 10 + root = ./.; 11 + fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ./.)) ( 12 + lib.fileset.unions [ 13 + ./Cargo.toml 14 + ./Cargo.lock 15 + ./src 16 + ] 17 + ); 18 + }; 19 + 20 + cargoLock.lockFile = ./Cargo.lock; 21 + 22 + meta = { 23 + inherit (toml) homepage description; 24 + license = lib.licenses.eupl12; 25 + maintainers = with lib.maintainers; [ isabelroses ]; 26 + mainProgram = "locker"; 27 + }; 28 + }
+27
flake.lock
··· 1 + { 2 + "nodes": { 3 + "nixpkgs": { 4 + "locked": { 5 + "lastModified": 1753432016, 6 + "narHash": "sha256-cnL5WWn/xkZoyH/03NNUS7QgW5vI7D1i74g48qplCvg=", 7 + "owner": "nixos", 8 + "repo": "nixpkgs", 9 + "rev": "6027c30c8e9810896b92429f0092f624f7b1aace", 10 + "type": "github" 11 + }, 12 + "original": { 13 + "owner": "nixos", 14 + "ref": "nixpkgs-unstable", 15 + "repo": "nixpkgs", 16 + "type": "github" 17 + } 18 + }, 19 + "root": { 20 + "inputs": { 21 + "nixpkgs": "nixpkgs" 22 + } 23 + } 24 + }, 25 + "root": "root", 26 + "version": 7 27 + }
+27
flake.nix
··· 1 + { 2 + inputs = { 3 + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 4 + }; 5 + 6 + outputs = 7 + { self, nixpkgs }: 8 + let 9 + forAllSystems = 10 + function: 11 + nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed ( 12 + system: function nixpkgs.legacyPackages.${system} 13 + ); 14 + in 15 + { 16 + packages = forAllSystems (pkgs: { 17 + locker = pkgs.callPackage ./default.nix { }; 18 + default = self.packages.${pkgs.stdenv.hostPlatform.system}.locker; 19 + }); 20 + 21 + devShells = forAllSystems (pkgs: { 22 + default = pkgs.callPackage ./shell.nix { }; 23 + }); 24 + 25 + overlays.default = final: _: { locker = final.callPackage ./default.nix { }; }; 26 + }; 27 + }
+26
shell.nix
··· 1 + { 2 + mkShell, 3 + callPackage, 4 + rustPlatform, 5 + 6 + # extra tooling 7 + clippy, 8 + rustfmt, 9 + rust-analyzer, 10 + }: 11 + let 12 + defaultPackage = callPackage ./default.nix { }; 13 + in 14 + mkShell { 15 + inputsFrom = [ defaultPackage ]; 16 + 17 + env = { 18 + RUST_SRC_PATH = rustPlatform.rustLibSrc; 19 + }; 20 + 21 + packages = [ 22 + clippy 23 + rustfmt 24 + rust-analyzer 25 + ]; 26 + }
+207
src/main.rs
··· 1 + use argh::FromArgs; 2 + use serde::Deserialize; 3 + use std::error::Error; 4 + use std::fs; 5 + use std::{collections::HashMap, path::PathBuf}; 6 + 7 + /// locker - a tool to lint your flake.lock file 8 + #[derive(FromArgs)] 9 + #[argh(help_triggers("-h", "--help"))] 10 + struct Args { 11 + #[argh(positional, default = "PathBuf::from(\"flake.lock\")")] 12 + flake_lock: PathBuf, 13 + } 14 + 15 + #[derive(Deserialize, Debug)] 16 + struct FlakeLock { 17 + nodes: HashMap<String, Node>, 18 + version: usize, 19 + 20 + #[allow(dead_code)] 21 + root: String, 22 + } 23 + 24 + #[derive(Deserialize, Debug)] 25 + struct Node { 26 + locked: Option<Locked>, 27 + } 28 + 29 + #[derive(Deserialize, Debug)] 30 + struct Locked { 31 + #[serde(rename = "type")] 32 + node_type: String, 33 + 34 + // for github, gitlab and sourcehut we have these fields 35 + owner: Option<String>, 36 + repo: Option<String>, 37 + 38 + // for git, hg and tarball we have these fields 39 + url: Option<String>, 40 + 41 + // path 42 + path: Option<String>, 43 + } 44 + 45 + fn main() -> Result<(), Box<dyn Error>> { 46 + let args: Args = argh::from_env(); 47 + let flake_lock_content = fs::read_to_string(&args.flake_lock)?; 48 + let flake_lock: FlakeLock = serde_json::from_str(&flake_lock_content)?; 49 + 50 + if flake_lock.version != 7 { 51 + eprintln!("Unsupported flake.lock version: {}", flake_lock.version); 52 + std::process::exit(1); 53 + } 54 + 55 + let inputs = parse_inputs(flake_lock); 56 + let duplicates = find_duplicates(inputs); 57 + 58 + if duplicates.is_empty() { 59 + println!("No duplicate inputs found."); 60 + std::process::exit(0); 61 + } 62 + 63 + println!("The following flake uris contained duplicate entries in your flake.lock:"); 64 + for (input, dups) in duplicates { 65 + eprintln!(" '{}': {}", input, dups.join(", ")); 66 + } 67 + 68 + std::process::exit(1); 69 + } 70 + 71 + fn parse_inputs(flake_lock: FlakeLock) -> HashMap<String, String> { 72 + let mut data = HashMap::new(); 73 + 74 + for (k, v) in flake_lock.nodes { 75 + if v.locked.is_none() { 76 + continue; 77 + } 78 + 79 + let val = flake_uri(v.locked.unwrap()).ok().unwrap_or_else(|| { 80 + eprintln!("Failed to parse URI for input '{k}'"); 81 + String::new() 82 + }); 83 + 84 + data.entry(k).insert_entry(val); 85 + } 86 + 87 + data 88 + } 89 + 90 + fn find_duplicates(inputs: HashMap<String, String>) -> HashMap<String, Vec<String>> { 91 + let mut seen: Vec<String> = Vec::new(); 92 + let mut duplicates: HashMap<String, Vec<String>> = HashMap::new(); 93 + 94 + for (input_name, input_uri) in inputs { 95 + if seen.contains(&input_uri) { 96 + duplicates.entry(input_uri).or_default().push(input_name); 97 + } else { 98 + seen.push(input_uri); 99 + } 100 + } 101 + 102 + duplicates 103 + } 104 + 105 + fn flake_uri(lock: Locked) -> Result<String, Box<dyn Error>> { 106 + match lock.node_type.as_str() { 107 + "github" | "gitlab" | "sourcehut" => Ok(format!( 108 + "{}:{}/{}", 109 + lock.node_type, 110 + lock.owner.unwrap().to_lowercase(), 111 + lock.repo.unwrap().to_lowercase() 112 + )), 113 + "git" | "hg" | "tarball" => Ok(format!( 114 + "{}:{}", 115 + lock.node_type, 116 + lock.url.unwrap_or_default() 117 + )), 118 + "path" => Ok(format!( 119 + "{}:{}", 120 + lock.node_type, 121 + lock.path.unwrap_or_default() 122 + )), 123 + _ => Err(format!("Unknown node type: {}", lock.node_type))?, 124 + } 125 + } 126 + 127 + #[cfg(test)] 128 + mod tests { 129 + use super::*; 130 + 131 + const FLAKE_LOCK: &str = r#" 132 + { 133 + "nodes": { 134 + "input1": { 135 + "locked": { 136 + "type": "github", 137 + "owner": "user1", 138 + "repo": "repo1" 139 + } 140 + }, 141 + "input2": { 142 + "locked": { 143 + "type": "github", 144 + "owner": "user2", 145 + "repo": "repo2" 146 + } 147 + }, 148 + "input3": { 149 + "locked": { 150 + "type": "github", 151 + "owner": "user1", 152 + "repo": "repo1" 153 + } 154 + }, 155 + "input4": { 156 + "locked": { 157 + "type": "git", 158 + "url": "https://example.com/repo.git" 159 + } 160 + }, 161 + "input5": { 162 + "locked": { 163 + "type": "git", 164 + "url": "https://example.com/repo.git" 165 + } 166 + } 167 + }, 168 + "version": 7, 169 + "root": "." 170 + } 171 + "#; 172 + 173 + #[test] 174 + fn test_parse_inputs() { 175 + let flake_lock: FlakeLock = serde_json::from_str(FLAKE_LOCK).unwrap(); 176 + let inputs = parse_inputs(flake_lock); 177 + 178 + assert_eq!(inputs.len(), 5); 179 + assert!(inputs.contains_key("input1")); 180 + assert!(inputs.contains_key("input2")); 181 + assert!(inputs.contains_key("input3")); 182 + assert!(inputs.contains_key("input4")); 183 + assert!(inputs.contains_key("input5")); 184 + 185 + assert_eq!(inputs.get("input1").unwrap(), "github:user1/repo1"); 186 + assert_eq!(inputs.get("input2").unwrap(), "github:user2/repo2"); 187 + assert_eq!(inputs.get("input3").unwrap(), "github:user1/repo1"); 188 + assert_eq!( 189 + inputs.get("input4").unwrap(), 190 + "git:https://example.com/repo.git" 191 + ); 192 + assert_eq!( 193 + inputs.get("input5").unwrap(), 194 + "git:https://example.com/repo.git" 195 + ); 196 + } 197 + 198 + #[test] 199 + fn test_duplicates() { 200 + let flake_lock: FlakeLock = serde_json::from_str(FLAKE_LOCK).unwrap(); 201 + 202 + let inputs = parse_inputs(flake_lock); 203 + let duplicates = find_duplicates(inputs.clone()); 204 + 205 + assert_eq!(duplicates.len(), 2); 206 + } 207 + }