├── .github └── CODEOWNERS ├── .gitignore ├── nix ├── devshell │ └── default.nix └── fmt │ └── default.nix ├── nix-editor.nix ├── default.nix ├── Cargo.toml ├── flake.lock ├── flake.nix ├── LICENSE ├── README.md ├── src ├── adder.rs ├── remover.rs ├── verify_getter.rs └── main.rs └── Cargo.lock /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @replit/devex -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /result 3 | 4 | replit.nix 5 | -------------------------------------------------------------------------------- /nix/devshell/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | rustc, 3 | rustfmt, 4 | mkShell, 5 | }: 6 | mkShell { 7 | name = "nix-editor"; 8 | packages = [ 9 | rustc 10 | rustfmt 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /nix-editor.nix: -------------------------------------------------------------------------------- 1 | { 2 | rustPlatform, 3 | rev, 4 | }: 5 | rustPlatform.buildRustPackage { 6 | pname = "nix-editor"; 7 | version = rev; 8 | 9 | cargoLock = { 10 | lockFile = ./Cargo.lock; 11 | }; 12 | 13 | src = builtins.path { 14 | path = ./.; 15 | name = "source"; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /nix/fmt/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | writeShellApplication, 3 | alejandra, 4 | rustfmt, 5 | }: 6 | writeShellApplication { 7 | name = "fmt"; 8 | text = '' 9 | echo "Formatting Nix code..." 10 | ${alejandra}/bin/alejandra -q . 11 | echo "Formatting Rust code..." 12 | ${rustfmt}/bin/cargo-fmt 13 | ''; 14 | } 15 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | # flake-compat makes Nix flakes compatible with the old Nix cli commands, like nix-build and nix-shell. 2 | (import ( 3 | fetchTarball { 4 | url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz"; 5 | sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; 6 | } 7 | ) { 8 | src = ./.; 9 | }) 10 | .defaultNix 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nix-editor" 3 | version = "0.3.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | rnix = "0.11.0" 10 | rowan = "0.15.11" 11 | serde_json = "1.0" 12 | serde = { version = "1.0", features = ["derive"] } 13 | clap = { version = "3.2.10", features = ["derive"] } 14 | anyhow = "1.0.58" 15 | 16 | [dev-dependencies] 17 | tempfile = "3.8.0" 18 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1696419054, 6 | "narHash": "sha256-EdR+dIKCfqL3voZUDYwcvgRDOektQB9KbhBVcE0/3Mo=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "7131f3c223a2d799568e4b278380cd9dac2b8579", 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 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A replit.nix editor."; 3 | 4 | inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 5 | 6 | outputs = { 7 | self, 8 | nixpkgs, 9 | }: let 10 | systems = [ 11 | "aarch64-darwin" 12 | "aarch64-linux" 13 | "x86_64-darwin" 14 | "x86_64-linux" 15 | ]; 16 | eachSystem = nixpkgs.lib.genAttrs systems; 17 | rev = 18 | if self ? rev 19 | then "0.0.0-${builtins.substring 0 7 self.rev}" 20 | else "0.0.0-dirty"; 21 | in { 22 | packages = eachSystem (system: let 23 | pkgs = nixpkgs.legacyPackages.${system}; 24 | in rec { 25 | default = nix-editor; 26 | nix-editor = pkgs.callPackage ./nix-editor.nix { 27 | inherit rev; 28 | }; 29 | devShell = pkgs.callPackage ./nix/devshell {}; 30 | fmt = pkgs.callPackage ./nix/fmt {}; 31 | }); 32 | devShells = eachSystem (system: { 33 | default = self.packages.${system}.devShell; 34 | }); 35 | formatter = eachSystem (system: self.packages.${system}.fmt); 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Replit, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This program edits Replit's replit.nix editor that is used by Goval to modify programatically interact with the file. 2 | 3 | It parses the file into an AST and traverses the AST to get the relevant information to modify the file. 4 | 5 | run `cargo run -- --help` to see what cli arguments are available. 6 | 7 | ``` 8 | nix-editor 9 | 10 | USAGE: 11 | nix-editor [OPTIONS] 12 | 13 | OPTIONS: 14 | -a, --add 15 | -d, --dep-type [default: regular] [possible values: regular, python] 16 | -h, --human 17 | --help Print help information 18 | -p, --path 19 | -r, --remove 20 | --return-output 21 | -v, --verbose 22 | -V, --version Print version information 23 | ``` 24 | 25 | You can directly add/remove packages through the cli args like so `cargo run -- --add pkgs.cowsay` or `cargo run -- --remove pkgs.cowsay` or `cargo run -- --get`. 26 | 27 | You can also run it without passing in any flags. If you do that, it reads json from stdin with the following structure: 28 | ``` 29 | {"op":"add", "dep": "pkgs.cowsay" } 30 | ``` 31 | 32 | # Contributing 33 | 34 | * Please run `nix fmt` to format the code in this repository before making a pull request. 35 | * `nix develop` will put you in a devshell with all the necessary development tools. 36 | -------------------------------------------------------------------------------- /src/adder.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use rnix::SyntaxNode; 3 | 4 | use crate::verify_getter::SyntaxNodeAndWhitespace; 5 | 6 | pub fn add_dep( 7 | deps_list: SyntaxNodeAndWhitespace, 8 | new_dep_opt: Option, 9 | ) -> Result { 10 | let new_dep = new_dep_opt.context("error: no dependency")?; 11 | let whitespace = deps_list.whitespace; 12 | let deps_list = deps_list.node; 13 | 14 | for dep in deps_list.children() { 15 | if dep.to_string() == new_dep { 16 | // dep is already present in the deps_list, we're done 17 | return Ok(deps_list); 18 | } 19 | } 20 | 21 | let mut base_indent = 0; 22 | if let Some(w) = whitespace { 23 | base_indent = w.text().replace("\n", "").len(); 24 | } 25 | let entry_indent = base_indent + 2; 26 | 27 | let has_newline = deps_list.to_string().contains('\n'); 28 | 29 | let newline = match has_newline { 30 | true => String::new(), 31 | false => std::iter::once("\n") 32 | .chain(std::iter::repeat(" ").take(base_indent)) 33 | .collect(), 34 | }; 35 | 36 | deps_list.splice_children( 37 | 1..1, 38 | vec![rnix::NodeOrToken::Node( 39 | rnix::Root::parse(&format!( 40 | "\n{}{}{newline}", 41 | &" ".repeat(entry_indent), 42 | new_dep 43 | )) 44 | .syntax() 45 | .clone_for_update(), 46 | )], 47 | ); 48 | 49 | Ok(deps_list) 50 | } 51 | 52 | #[cfg(test)] 53 | mod add_tests { 54 | use super::*; 55 | use crate::verify_getter::verify_get; 56 | use crate::DepType; 57 | 58 | fn test_add(dep_type: DepType, new_dep: &str, initial_contents: &str, expected_contents: &str) { 59 | let tree = rnix::Root::parse(&initial_contents) 60 | .syntax() 61 | .clone_for_update(); 62 | 63 | let deps_list_res = verify_get(&tree, dep_type); 64 | assert!(deps_list_res.is_ok()); 65 | 66 | let deps_list = deps_list_res.unwrap(); 67 | 68 | let new_deps_list = add_dep(deps_list, Some(new_dep.to_string())); 69 | assert!(new_deps_list.is_ok()); 70 | 71 | assert_eq!(tree.to_string(), expected_contents.to_string()); 72 | } 73 | 74 | #[test] 75 | fn test_empty_regular_add_dep() { 76 | test_add( 77 | DepType::Regular, 78 | "pkgs.test", 79 | r#"{ pkgs }: { 80 | deps = []; 81 | } 82 | "#, 83 | r#"{ pkgs }: { 84 | deps = [ 85 | pkgs.test 86 | ]; 87 | } 88 | "#, 89 | ) 90 | } 91 | 92 | #[test] 93 | fn test_weird_empty_regular_add_dep() { 94 | test_add( 95 | DepType::Regular, 96 | "pkgs.test", 97 | r#"{ pkgs }: { deps = []; }"#, 98 | r#"{ pkgs }: { deps = [ 99 | pkgs.test 100 | ]; }"#, 101 | ) 102 | } 103 | 104 | #[test] 105 | fn test_empty_but_expanded_regular_add_dep() { 106 | test_add( 107 | DepType::Regular, 108 | "pkgs.test", 109 | r#"{ pkgs }: { 110 | deps = [ 111 | ]; 112 | }"#, 113 | r#"{ pkgs }: { 114 | deps = [ 115 | pkgs.test 116 | ]; 117 | }"#, 118 | ) 119 | } 120 | 121 | #[test] 122 | fn test_duplicate_add() { 123 | test_add( 124 | DepType::Regular, 125 | "pkgs.test", 126 | r#"{ pkgs }: { 127 | deps = [ 128 | pkgs.test 129 | ]; 130 | } 131 | "#, 132 | r#"{ pkgs }: { 133 | deps = [ 134 | pkgs.test 135 | ]; 136 | } 137 | "#, 138 | ) 139 | } 140 | 141 | #[test] 142 | fn test_with_pkgs_add() { 143 | test_add( 144 | DepType::Regular, 145 | "pkgs.ncdu", 146 | r#"{ pkgs }: { 147 | deps = with pkgs; [ 148 | test 149 | ]; 150 | } 151 | "#, 152 | r#"{ pkgs }: { 153 | deps = with pkgs; [ 154 | pkgs.ncdu 155 | test 156 | ]; 157 | } 158 | "#, 159 | ) 160 | } 161 | 162 | const PYTHON_REPLIT_NIX: &str = r#"{ pkgs }: { 163 | deps = [ 164 | pkgs.python38Full 165 | ]; 166 | env = { 167 | PYTHON_LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ 168 | pkgs.stdenv.cc.cc.lib 169 | pkgs.zlib 170 | pkgs.glib 171 | pkgs.xorg.libX11 172 | ]; 173 | PYTHONBIN = "${pkgs.python38Full}/bin/python3.8"; 174 | LANG = "en_US.UTF-8"; 175 | }; 176 | }"#; 177 | 178 | #[test] 179 | fn test_regular_add_dep() { 180 | test_add( 181 | DepType::Regular, 182 | "pkgs.test", 183 | PYTHON_REPLIT_NIX, 184 | r#"{ pkgs }: { 185 | deps = [ 186 | pkgs.test 187 | pkgs.python38Full 188 | ]; 189 | env = { 190 | PYTHON_LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ 191 | pkgs.stdenv.cc.cc.lib 192 | pkgs.zlib 193 | pkgs.glib 194 | pkgs.xorg.libX11 195 | ]; 196 | PYTHONBIN = "${pkgs.python38Full}/bin/python3.8"; 197 | LANG = "en_US.UTF-8"; 198 | }; 199 | }"#, 200 | ); 201 | } 202 | 203 | #[test] 204 | fn test_python_add_dep() { 205 | test_add( 206 | DepType::Python, 207 | "pkgs.test", 208 | PYTHON_REPLIT_NIX, 209 | r#"{ pkgs }: { 210 | deps = [ 211 | pkgs.python38Full 212 | ]; 213 | env = { 214 | PYTHON_LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ 215 | pkgs.test 216 | pkgs.stdenv.cc.cc.lib 217 | pkgs.zlib 218 | pkgs.glib 219 | pkgs.xorg.libX11 220 | ]; 221 | PYTHONBIN = "${pkgs.python38Full}/bin/python3.8"; 222 | LANG = "en_US.UTF-8"; 223 | }; 224 | }"#, 225 | ); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/remover.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use rnix::{SyntaxNode, TextRange}; 3 | 4 | pub fn remove_dep( 5 | contents: &str, 6 | deps_list: SyntaxNode, 7 | remove_dep_opt: Option, 8 | ) -> Result { 9 | let remove_dep = remove_dep_opt.context("error: expected dep to remove")?; 10 | 11 | let search = find_remove_dep(deps_list, &remove_dep); 12 | if search.is_err() { 13 | return Ok(contents.to_string()); 14 | } 15 | let range_to_remove = search?; 16 | let text_start: usize = range_to_remove.start().into(); 17 | 18 | // since there may be leading white space, we need to remove the leading white space 19 | // go backwards char by char until we find non whitespace char 20 | let remove_start: usize = search_backwards_non_whitespace(text_start, contents); 21 | let remove_end: usize = range_to_remove.end().into(); 22 | 23 | let (before, rest) = contents.split_at(remove_start); 24 | let (_, after) = rest.split_at(remove_end - remove_start); 25 | 26 | Ok(format!("{}{}", before, after)) 27 | } 28 | 29 | fn search_backwards_non_whitespace(start_pos: usize, contents: &str) -> usize { 30 | let mut pos = start_pos; 31 | while pos > 0 { 32 | let c = contents.chars().nth(pos - 1).unwrap(); 33 | if !c.is_whitespace() { 34 | return pos; 35 | } 36 | pos -= 1; 37 | } 38 | 0 39 | } 40 | 41 | fn find_remove_dep(deps_list: SyntaxNode, remove_dep: &str) -> Result { 42 | let mut deps = deps_list.children(); 43 | 44 | let dep = deps 45 | .find(|dep| dep.text() == remove_dep) 46 | .context("error: could not find dep to remove")?; 47 | 48 | Ok(dep.text_range()) 49 | } 50 | 51 | #[cfg(test)] 52 | mod remove_tests { 53 | use super::*; 54 | use crate::verify_getter::verify_get; 55 | use crate::DepType; 56 | 57 | fn python_replit_nix() -> String { 58 | r#" 59 | { pkgs }: { 60 | deps = [ 61 | pkgs.python38Full 62 | ]; 63 | env = { 64 | PYTHON_LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ 65 | pkgs.stdenv.cc.cc.lib 66 | pkgs.zlib 67 | pkgs.glib 68 | pkgs.xorg.libX11 69 | ]; 70 | PYTHONBIN = "${pkgs.python38Full}/bin/python3.8"; 71 | LANG = "en_US.UTF-8"; 72 | }; 73 | } 74 | "# 75 | .to_string() 76 | } 77 | 78 | #[test] 79 | fn test_regular_remove_with_pkgs_dep() { 80 | let contents = r#"{ pkgs }: { 81 | deps = with pkgs; [ 82 | pkgs.ncdu 83 | test 84 | ]; 85 | } 86 | "#; 87 | 88 | let tree = rnix::Root::parse(&contents).syntax(); 89 | let deps_list_res = verify_get(&tree, DepType::Regular); 90 | assert!(deps_list_res.is_ok()); 91 | 92 | let deps_list = deps_list_res.unwrap(); 93 | 94 | let dep_to_remove = "pkgs.ncdu"; 95 | 96 | let new_contents = remove_dep(&contents, deps_list.node, Some(dep_to_remove.to_string())); 97 | assert!(new_contents.is_ok()); 98 | 99 | let new_contents = new_contents.unwrap(); 100 | 101 | let expected_contents = r#"{ pkgs }: { 102 | deps = with pkgs; [ 103 | test 104 | ]; 105 | } 106 | "#; 107 | assert_eq!(new_contents, expected_contents); 108 | } 109 | 110 | #[test] 111 | fn test_remove_idempotent_dep() { 112 | let contents = r#"{ pkgs }: { 113 | deps = with pkgs; [ 114 | ]; 115 | } 116 | "#; 117 | 118 | let tree = rnix::Root::parse(&contents).syntax(); 119 | let deps_list_res = verify_get(&tree, DepType::Regular); 120 | assert!(deps_list_res.is_ok()); 121 | 122 | let deps_list = deps_list_res.unwrap(); 123 | 124 | let dep_to_remove = "pkgs.cowsay"; 125 | 126 | let new_contents = remove_dep(&contents, deps_list.node, Some(dep_to_remove.to_string())); 127 | assert!(new_contents.is_ok()); 128 | 129 | let new_contents = new_contents.unwrap(); 130 | 131 | assert_eq!(new_contents, contents); 132 | } 133 | 134 | #[test] 135 | fn test_regular_remove_dep() { 136 | let contents = python_replit_nix(); 137 | let tree = rnix::Root::parse(&contents).syntax(); 138 | let deps_list_res = verify_get(&tree, DepType::Regular); 139 | assert!(deps_list_res.is_ok()); 140 | 141 | let deps_list = deps_list_res.unwrap(); 142 | 143 | let dep_to_remove = "pkgs.python38Full"; 144 | 145 | let new_contents = remove_dep(&contents, deps_list.node, Some(dep_to_remove.to_string())); 146 | assert!(new_contents.is_ok()); 147 | 148 | let new_contents = new_contents.unwrap(); 149 | 150 | let expected_contents = r#" 151 | { pkgs }: { 152 | deps = [ 153 | ]; 154 | env = { 155 | PYTHON_LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ 156 | pkgs.stdenv.cc.cc.lib 157 | pkgs.zlib 158 | pkgs.glib 159 | pkgs.xorg.libX11 160 | ]; 161 | PYTHONBIN = "${pkgs.python38Full}/bin/python3.8"; 162 | LANG = "en_US.UTF-8"; 163 | }; 164 | } 165 | "# 166 | .to_string(); 167 | assert_eq!(new_contents, expected_contents); 168 | } 169 | 170 | #[test] 171 | fn test_python_remove_dep() { 172 | let contents = python_replit_nix(); 173 | let tree = rnix::Root::parse(&contents).syntax(); 174 | let deps_list_res = verify_get(&tree, DepType::Python); 175 | assert!(deps_list_res.is_ok()); 176 | 177 | let deps_list = deps_list_res.unwrap(); 178 | 179 | let dep_to_remove = "pkgs.glib"; 180 | 181 | let new_contents = remove_dep(&contents, deps_list.node, Some(dep_to_remove.to_string())); 182 | assert!(new_contents.is_ok()); 183 | 184 | let new_contents = new_contents.unwrap(); 185 | 186 | let expected_contents = r#" 187 | { pkgs }: { 188 | deps = [ 189 | pkgs.python38Full 190 | ]; 191 | env = { 192 | PYTHON_LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ 193 | pkgs.stdenv.cc.cc.lib 194 | pkgs.zlib 195 | pkgs.xorg.libX11 196 | ]; 197 | PYTHONBIN = "${pkgs.python38Full}/bin/python3.8"; 198 | LANG = "en_US.UTF-8"; 199 | }; 200 | } 201 | "# 202 | .to_string(); 203 | assert_eq!(new_contents, expected_contents); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/verify_getter.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Context, Result}; 2 | use rnix::*; 3 | 4 | use crate::{DepType, EMPTY_TEMPLATE}; 5 | 6 | // kind of like assert! but returns an error instead of panicking 7 | macro_rules! verify_eq { 8 | ($a:expr, $b:expr) => { 9 | if $a != $b { 10 | bail!( 11 | "error: expected {} but got {}", 12 | stringify!($b), 13 | stringify!($a) 14 | ); 15 | } 16 | }; 17 | } 18 | 19 | #[derive(Debug)] 20 | pub struct SyntaxNodeAndWhitespace { 21 | pub whitespace: Option, 22 | pub node: SyntaxNode, 23 | } 24 | 25 | // Will try to parse through the AST and return a list of deps 26 | // If at any point, the tree is not *exactly* how we expect it to look, 27 | // it will return an error. Since nix is so complex, we have to require some 28 | // assumptions about the AST, or else it'll be impossible to do anything. 29 | pub fn verify_get(root: &SyntaxNode, dep_type: DepType) -> Result { 30 | verify_eq!(root.kind(), SyntaxKind::NODE_ROOT); 31 | 32 | if root.children().count() == 0 { 33 | root.splice_children(0..0, vec![rnix::NodeOrToken::Node(template_empty())]); 34 | } 35 | 36 | let lambda = get_nth_child(&root, 0).context("expected to have a child")?; 37 | verify_eq!(lambda.kind(), SyntaxKind::NODE_LAMBDA); 38 | 39 | let arg_pattern = get_nth_child(&lambda, 0).context("expected to have a child")?; 40 | verify_eq!(arg_pattern.kind(), SyntaxKind::NODE_PATTERN); 41 | 42 | if find_child_with_value(&arg_pattern, "pkgs").is_none() { 43 | bail!("error: expected pkgs"); 44 | } 45 | 46 | let attr_set = get_nth_child(&lambda, 1).context("expected to have two children")?; 47 | verify_eq!(attr_set.kind(), SyntaxKind::NODE_ATTR_SET); 48 | 49 | let deps_list = match dep_type { 50 | DepType::Regular => verify_get_regular(&attr_set)?, 51 | DepType::Python => verify_get_python(&attr_set)?, 52 | }; 53 | 54 | Ok(deps_list) 55 | } 56 | 57 | fn verify_get_regular(attr_set: &SyntaxNode) -> Result { 58 | let deps = find_or_insert_key_value_with_key(&attr_set, "deps", template_deps()) 59 | .context("expected to have a deps key")?; 60 | let whitespace = deps.whitespace; 61 | let deps = deps.node; 62 | verify_eq!(deps.kind(), SyntaxKind::NODE_ATTRPATH_VALUE); 63 | 64 | let value = get_nth_child(&deps, 1).context("expected to have two children")?; 65 | 66 | let deps_list = match value.kind() { 67 | SyntaxKind::NODE_LIST => value, 68 | SyntaxKind::NODE_WITH => { 69 | get_nth_child(&value, 1).context("expected to have at least two children")? 70 | } 71 | _ => bail!("unexpected value for deps, expected either with pkgs; or a list"), 72 | }; 73 | verify_eq!(deps_list.kind(), SyntaxKind::NODE_LIST); 74 | 75 | Ok(SyntaxNodeAndWhitespace { 76 | whitespace, 77 | node: deps_list, 78 | }) 79 | } 80 | 81 | fn find_or_insert_key_value_with_key( 82 | node: &SyntaxNode, 83 | key: &str, 84 | if_missing_template: SyntaxNode, 85 | ) -> Option { 86 | let found = find_key_value_with_key(&node, key); 87 | if found.is_some() { 88 | return found; 89 | } 90 | let count = node.children().count() + 2; 91 | 92 | node.splice_children( 93 | count..count, 94 | vec![ 95 | rnix::NodeOrToken::Node(rnix::Root::parse("\n ").syntax().clone_for_update()), 96 | rnix::NodeOrToken::Node(if_missing_template), 97 | ], 98 | ); 99 | 100 | let result = find_key_value_with_key(&node, key); 101 | result 102 | } 103 | 104 | fn template_empty() -> SyntaxNode { 105 | let ast = rnix::Root::parse(EMPTY_TEMPLATE); 106 | let errors = ast.errors(); 107 | if errors.len() > 0 { 108 | panic!("template_empty had an error: {:#?}", errors) 109 | } 110 | ast.syntax().first_child().unwrap().clone_for_update() 111 | } 112 | 113 | fn template_deps() -> SyntaxNode { 114 | let python_env_template = r#"{ 115 | deps = []; 116 | }"#; 117 | let ast = rnix::Root::parse(python_env_template); 118 | let errors = ast.errors(); 119 | if errors.len() > 0 { 120 | panic!("template_deps had an error: {:#?}", errors) 121 | } 122 | ast.syntax() 123 | .first_child() 124 | .unwrap() 125 | .first_child() 126 | .unwrap() 127 | .clone_for_update() 128 | } 129 | 130 | fn template_env() -> SyntaxNode { 131 | let python_env_template = r#"{ 132 | env = { 133 | PYTHON_LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath []; 134 | }; 135 | }"#; 136 | let ast = rnix::Root::parse(python_env_template); 137 | let errors = ast.errors(); 138 | if errors.len() > 0 { 139 | panic!("template_env had an error: {:#?}", errors) 140 | } 141 | ast.syntax() 142 | .first_child() 143 | .unwrap() 144 | .first_child() 145 | .unwrap() 146 | .clone_for_update() 147 | } 148 | 149 | fn template_python() -> SyntaxNode { 150 | let python_env_template = r#"{ 151 | PYTHON_LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath []; 152 | }"#; 153 | let ast = rnix::Root::parse(python_env_template); 154 | let errors = ast.errors(); 155 | if errors.len() > 0 { 156 | panic!("template_python had an error: {:#?}", errors) 157 | } 158 | ast.syntax() 159 | .first_child() 160 | .unwrap() 161 | .first_child() 162 | .unwrap() 163 | .clone_for_update() 164 | } 165 | 166 | fn verify_get_python(attr_set: &SyntaxNode) -> Result { 167 | let env = find_or_insert_key_value_with_key(&attr_set, "env", template_env()) 168 | .context("expected to have env key")? 169 | .node; 170 | verify_eq!(env.kind(), SyntaxKind::NODE_ATTRPATH_VALUE); 171 | 172 | let env_attr_set = get_nth_child(&env, 1).context("expected to have two children")?; 173 | verify_eq!(env_attr_set.kind(), SyntaxKind::NODE_ATTR_SET); 174 | 175 | let py_lib_path = find_or_insert_key_value_with_key( 176 | &env_attr_set, 177 | "PYTHON_LD_LIBRARY_PATH", 178 | template_python(), 179 | ) 180 | .context("expected to have PYTHON_LD_LIBRARY_PATH key")?; 181 | let whitespace = py_lib_path.whitespace; 182 | let py_lib_path = py_lib_path.node; 183 | verify_eq!(py_lib_path.kind(), SyntaxKind::NODE_ATTRPATH_VALUE); 184 | 185 | let py_lib_apply = get_nth_child(&py_lib_path, 1).context("expected to have two children")?; 186 | verify_eq!(py_lib_apply.kind(), SyntaxKind::NODE_APPLY); 187 | 188 | let py_lib_node_select = get_nth_child(&py_lib_apply, 0).context("expected to have a child")?; 189 | verify_eq!(py_lib_node_select.kind(), SyntaxKind::NODE_SELECT); 190 | verify_eq!(py_lib_node_select.text(), "pkgs.lib.makeLibraryPath"); 191 | 192 | let py_lib_node_list = 193 | get_nth_child(&py_lib_apply, 1).context("expected to have two children")?; 194 | verify_eq!(py_lib_node_list.kind(), SyntaxKind::NODE_LIST); 195 | 196 | Ok(SyntaxNodeAndWhitespace { 197 | whitespace, 198 | node: py_lib_node_list, 199 | }) 200 | } 201 | 202 | fn get_nth_child(node: &SyntaxNode, index: usize) -> Option { 203 | node.children().into_iter().nth(index) 204 | } 205 | 206 | fn find_child_with_value(node: &SyntaxNode, name: &str) -> Option { 207 | node.children() 208 | .into_iter() 209 | .find(|child| child.text() == name) 210 | } 211 | 212 | fn find_key_value_with_key(node: &SyntaxNode, key: &str) -> Option { 213 | if node.kind() != SyntaxKind::NODE_ATTR_SET { 214 | return None; 215 | } 216 | 217 | let mut last_whitespace = None; 218 | 219 | let node = node.children_with_tokens().into_iter().find(|child| { 220 | if let Some(token) = child.as_token() { 221 | if token.kind() != SyntaxKind::TOKEN_WHITESPACE { 222 | return false; 223 | } 224 | let w = token.text(); 225 | if !w.contains("\n") { 226 | return false; 227 | } 228 | last_whitespace = Some(token.clone()); 229 | return false; 230 | } 231 | if child.as_node().is_none() { 232 | return false; 233 | } 234 | 235 | let node = child.as_node().unwrap(); 236 | 237 | if node.kind() != SyntaxKind::NODE_ATTRPATH_VALUE { 238 | return false; 239 | } 240 | 241 | let key_node = match get_nth_child(node, 0) { 242 | Some(child) => child, 243 | None => return false, 244 | }; 245 | 246 | key_node.text() == key 247 | }); 248 | 249 | match node { 250 | Some(node_or_token) => Some(SyntaxNodeAndWhitespace { 251 | whitespace: last_whitespace, 252 | node: node_or_token.as_node().unwrap().clone(), 253 | }), 254 | _ => None, 255 | } 256 | } 257 | 258 | // unit tests 259 | #[cfg(test)] 260 | mod verify_get_tests { 261 | use super::*; 262 | 263 | const PYTHON_REPLIT_NIX: &str = r#"{ pkgs }: { 264 | deps = [ 265 | pkgs.python38Full 266 | ]; 267 | env = { 268 | PYTHON_LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ 269 | # Needed for pandas / numpy 270 | pkgs.stdenv.cc.cc.lib 271 | pkgs.zlib 272 | # Needed for pygame 273 | pkgs.glib 274 | # Needed for matplotlib 275 | pkgs.xorg.libX11 276 | ]; 277 | PYTHONBIN = "${pkgs.python38Full}/bin/python3.8"; 278 | LANG = "en_US.UTF-8"; 279 | }; 280 | }"#; 281 | 282 | fn gets_ok(code: &str, dep_type: DepType) -> SyntaxNodeAndWhitespace { 283 | let ast = rnix::Root::parse(code).syntax().clone_for_update(); 284 | let deps_list_res = verify_get(&ast, dep_type); 285 | assert!(deps_list_res.is_ok()); 286 | deps_list_res.unwrap() 287 | } 288 | 289 | #[test] 290 | fn verify_get_when_missing_everything() { 291 | let deps_list = gets_ok(r#" "#, DepType::Regular); 292 | let deps_list = deps_list.node; 293 | let deps_list_children: Vec = deps_list.children().collect(); 294 | assert_eq!(deps_list_children.len(), 0); 295 | } 296 | 297 | #[test] 298 | fn verify_get_when_missing_deps() { 299 | let deps_list = gets_ok(r#"{ pkgs }: {}"#, DepType::Regular); 300 | let deps_list = deps_list.node; 301 | let deps_list_children: Vec = deps_list.children().collect(); 302 | assert_eq!(deps_list_children.len(), 0); 303 | } 304 | 305 | #[test] 306 | fn verify_get_when_missing_env() { 307 | let deps_list = gets_ok( 308 | r#"{ pkgs }: { 309 | deps = []; 310 | }"#, 311 | DepType::Python, 312 | ); 313 | let deps_list = deps_list.node; 314 | let deps_list_children: Vec = deps_list.children().collect(); 315 | assert_eq!(deps_list_children.len(), 0); 316 | } 317 | 318 | #[test] 319 | fn verify_get_when_missing_python() { 320 | let deps_list = gets_ok( 321 | r#"{ pkgs }: { 322 | deps = []; 323 | env = {}; 324 | }"#, 325 | DepType::Python, 326 | ); 327 | let deps_list = deps_list.node; 328 | let deps_list_children: Vec = deps_list.children().collect(); 329 | assert_eq!(deps_list_children.len(), 0); 330 | } 331 | 332 | #[test] 333 | fn verify_get_python() { 334 | let deps_list = gets_ok(PYTHON_REPLIT_NIX, DepType::Python); 335 | 336 | let whitespace = deps_list.whitespace.unwrap(); 337 | assert_eq!(whitespace.to_string().len(), 5); 338 | 339 | let deps_list = deps_list.node; 340 | let deps_list_children: Vec = deps_list.children().collect(); 341 | 342 | assert_eq!(deps_list_children.len(), 4); 343 | 344 | let deps_list_children_names = deps_list_children 345 | .iter() 346 | .map(|child| child.text()) 347 | .collect::>(); 348 | assert_eq!( 349 | deps_list_children_names, 350 | vec![ 351 | "pkgs.stdenv.cc.cc.lib", 352 | "pkgs.zlib", 353 | "pkgs.glib", 354 | "pkgs.xorg.libX11" 355 | ] 356 | ); 357 | 358 | for child in deps_list_children { 359 | assert_eq!(child.kind(), SyntaxKind::NODE_SELECT); 360 | } 361 | } 362 | 363 | #[test] 364 | fn verify_get_regular() { 365 | let deps_list = gets_ok(PYTHON_REPLIT_NIX, DepType::Regular); 366 | let deps_list = deps_list.node; 367 | let deps_list_children: Vec = deps_list.children().collect(); 368 | 369 | assert_eq!(deps_list_children.len(), 1); 370 | assert_eq!(deps_list_children[0].text(), "pkgs.python38Full"); 371 | assert_eq!(deps_list_children[0].kind(), SyntaxKind::NODE_SELECT); 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod adder; 2 | mod remover; 3 | mod verify_getter; 4 | 5 | use anyhow::Result; 6 | use rnix::SyntaxNode; 7 | 8 | use std::fs; 9 | use std::{env, io, io::prelude::*, path::Path}; 10 | 11 | use serde::{Deserialize, Serialize}; 12 | use serde_json::{from_str, to_string}; 13 | 14 | use clap::{ArgEnum, Parser}; 15 | 16 | use crate::adder::add_dep; 17 | use crate::remover::remove_dep; 18 | use crate::verify_getter::verify_get; 19 | 20 | #[derive(Parser, Debug, Default, Clone)] 21 | #[clap(author, version, about, long_about = None)] 22 | struct Args { 23 | // dep to add 24 | #[clap(short, long, value_parser)] 25 | add: Option, 26 | 27 | // dep to remove 28 | #[clap(short, long, value_parser)] 29 | remove: Option, 30 | 31 | // print current deps 32 | #[clap(short, long, value_parser, default_value = "false")] 33 | get: bool, 34 | 35 | // filepath for replit.nix file 36 | #[clap(short, long, value_parser)] 37 | path: Option, 38 | 39 | // human readable output 40 | #[clap(short, long, value_parser, default_value = "false")] 41 | human: bool, 42 | 43 | // dep type - used for setting special dep types in the replit.nix file 44 | #[clap(short, long, arg_enum, default_value = "regular")] 45 | dep_type: DepType, 46 | 47 | // verbose output 48 | #[clap(short, long, value_parser, default_value = "false")] 49 | verbose: bool, 50 | 51 | // Whether or not to write this value directly to the file, 52 | // or just print it as part of the return message 53 | #[clap(long, value_parser, default_value = "false")] 54 | return_output: bool, 55 | } 56 | 57 | #[derive(Serialize, Deserialize, Debug)] 58 | enum OpKind { 59 | #[serde(rename = "add")] 60 | Add, 61 | 62 | #[serde(rename = "remove")] 63 | Remove, 64 | 65 | #[serde(rename = "get")] 66 | Get, 67 | } 68 | 69 | #[derive(Serialize, Deserialize, ArgEnum, Clone, Copy, Debug)] 70 | pub enum DepType { 71 | #[serde(rename = "regular")] 72 | Regular, 73 | 74 | #[serde(rename = "python")] 75 | Python, 76 | } 77 | 78 | impl Default for DepType { 79 | fn default() -> Self { 80 | DepType::Regular 81 | } 82 | } 83 | 84 | #[derive(Serialize, Deserialize)] 85 | struct Op { 86 | op: OpKind, 87 | dep_type: Option, 88 | dep: Option, 89 | } 90 | 91 | #[derive(Serialize, Deserialize)] 92 | struct Res { 93 | status: String, 94 | data: Option, 95 | } 96 | 97 | fn main() { 98 | // handle command line args 99 | let args = Args::parse(); 100 | real_main(&mut io::stdout(), args) 101 | } 102 | 103 | fn real_main(stdout: &mut W, args: Args) { 104 | let replit_nix_file = "./replit.nix"; 105 | let default_replit_nix_filepath: String = match env::var("REPL_HOME") { 106 | Ok(repl_home) => Path::new(repl_home.as_str()) 107 | .join(replit_nix_file) 108 | .to_str() 109 | .unwrap() 110 | .to_string(), 111 | Err(_) => replit_nix_file.to_string(), 112 | }; 113 | 114 | let replit_nix_filepath = args.path.unwrap_or_else(|| default_replit_nix_filepath); 115 | 116 | let human_readable = args.human; 117 | let verbose = args.verbose; 118 | 119 | if args.get { 120 | if verbose { 121 | writeln!(stdout, "get_dep").unwrap(); 122 | } 123 | 124 | let (status, data) = perform_op( 125 | stdout, 126 | OpKind::Get, 127 | None, 128 | args.dep_type, 129 | &replit_nix_filepath, 130 | verbose, 131 | args.return_output, 132 | ); 133 | send_res(stdout, &status, data, human_readable); 134 | return; 135 | } 136 | 137 | // if user explicitly passes in a add or remove dep, then we only handle that specific op 138 | if let Some(add_dep) = args.add { 139 | if verbose { 140 | writeln!(stdout, "add_dep").unwrap(); 141 | } 142 | 143 | let (status, data) = perform_op( 144 | stdout, 145 | OpKind::Add, 146 | Some(add_dep), 147 | args.dep_type, 148 | &replit_nix_filepath, 149 | verbose, 150 | args.return_output, 151 | ); 152 | send_res(stdout, &status, data, human_readable); 153 | return; 154 | } 155 | 156 | if let Some(remove_dep) = args.remove { 157 | if verbose { 158 | writeln!(stdout, "remove_dep").unwrap(); 159 | } 160 | 161 | let (status, data) = perform_op( 162 | stdout, 163 | OpKind::Remove, 164 | Some(remove_dep), 165 | args.dep_type, 166 | &replit_nix_filepath, 167 | verbose, 168 | args.return_output, 169 | ); 170 | send_res(stdout, &status, data, human_readable); 171 | return; 172 | } 173 | 174 | if verbose { 175 | writeln!(stdout, "reading from stdin").unwrap(); 176 | } 177 | 178 | let stdin = io::stdin(); 179 | for line in stdin.lock().lines() { 180 | match line { 181 | Ok(line) => { 182 | let json: Op = match from_str(&line) { 183 | Ok(json_val) => json_val, 184 | Err(_) => { 185 | send_res( 186 | stdout, 187 | "error", 188 | Some("Invalid JSON".to_string()), 189 | human_readable, 190 | ); 191 | continue; 192 | } 193 | }; 194 | 195 | let (status, data) = perform_op( 196 | stdout, 197 | json.op, 198 | json.dep, 199 | json.dep_type.unwrap_or(args.dep_type), 200 | &replit_nix_filepath, 201 | verbose, 202 | args.return_output, 203 | ); 204 | send_res(stdout, &status, data, human_readable); 205 | } 206 | Err(_) => { 207 | send_res( 208 | stdout, 209 | "error", 210 | Some("Could not read stdin".to_string()), 211 | human_readable, 212 | ); 213 | } 214 | } 215 | } 216 | } 217 | 218 | const EMPTY_TEMPLATE: &str = r#"{pkgs}: { 219 | deps = []; 220 | } 221 | "#; 222 | 223 | fn perform_op( 224 | stdout: &mut W, 225 | op: OpKind, 226 | dep: Option, 227 | dep_type: DepType, 228 | replit_nix_filepath: &str, 229 | verbose: bool, 230 | return_output: bool, 231 | ) -> (String, Option) { 232 | if verbose { 233 | writeln!(stdout, "perform_op: {:?} {:?}", op, dep).unwrap(); 234 | } 235 | 236 | // read replit.nix file 237 | let contents = match fs::read_to_string(replit_nix_filepath) { 238 | Ok(contents) => contents, 239 | // if replit.nix doesn't exist start with an empty one 240 | Err(err) if err.kind() == io::ErrorKind::NotFound => EMPTY_TEMPLATE.to_string(), 241 | Err(_) => { 242 | return ( 243 | "error".to_string(), 244 | Some(format!("error: reading file - {:?}", &replit_nix_filepath)), 245 | ) 246 | } 247 | }; 248 | 249 | let root = rnix::Root::parse(&contents).syntax().clone_for_update(); 250 | 251 | let deps_list = match verify_get(&root, dep_type) { 252 | Ok(deps_list) => deps_list, 253 | Err(_) => { 254 | return ( 255 | "error".to_string(), 256 | Some("Could not verify and get".to_string()), 257 | ); 258 | } 259 | }; 260 | 261 | let op_res = match op { 262 | OpKind::Add => add_dep(deps_list, dep).map(|_| root.to_string()), 263 | OpKind::Remove => remove_dep(&contents, deps_list.node, dep), 264 | OpKind::Get => { 265 | let deps = match get_deps(deps_list.node) { 266 | Ok(deps) => deps, 267 | Err(_) => { 268 | return ("error".to_string(), Some("Could not get deps".to_string())); 269 | } 270 | }; 271 | return ("success".to_string(), Some(deps.join(","))); 272 | } 273 | }; 274 | 275 | let new_contents = match op_res { 276 | Ok(new_contents) => new_contents, 277 | Err(_) => { 278 | return ( 279 | "error".to_string(), 280 | Some("Could not perform op".to_string()), 281 | ); 282 | } 283 | }; 284 | 285 | if return_output { 286 | return ("success".to_string(), Some(new_contents)); 287 | } 288 | 289 | if new_contents == contents { 290 | return ("success".to_string(), None); 291 | } 292 | 293 | // write new replit.nix file 294 | match fs::write(&replit_nix_filepath, new_contents) { 295 | Ok(_) => ("success".to_string(), None), 296 | Err(err) => ( 297 | "error".to_string(), 298 | Some(format!( 299 | "Could not write to file {}: {}", 300 | replit_nix_filepath, err 301 | )), 302 | ), 303 | } 304 | } 305 | 306 | fn send_res( 307 | stdout: &mut W, 308 | status: &str, 309 | data: Option, 310 | human_readable: bool, 311 | ) { 312 | if human_readable { 313 | let mut out = status.to_owned(); 314 | 315 | if let Some(data) = data { 316 | out += &(": ".to_string() + &data); 317 | } 318 | writeln!(stdout, "{}", out).unwrap(); 319 | return; 320 | } 321 | 322 | let res = Res { 323 | status: status.to_string(), 324 | data, 325 | }; 326 | 327 | let json = match to_string(&res) { 328 | Ok(json) => json, 329 | Err(_) => { 330 | if human_readable { 331 | writeln!(stdout, "error: Could not serialize to JSON").unwrap(); 332 | } else { 333 | let err_msg = r#"{"status": "error", "data": "Could not serialize to JSON"}"#; 334 | writeln!(stdout, "{}", err_msg).unwrap(); 335 | } 336 | return; 337 | } 338 | }; 339 | 340 | writeln!(stdout, "{}", json).unwrap(); 341 | } 342 | 343 | fn get_deps(deps_list: SyntaxNode) -> Result> { 344 | Ok(deps_list 345 | .children() 346 | .map(|child| child.text().to_string()) 347 | .collect()) 348 | } 349 | 350 | #[cfg(test)] 351 | mod integration_tests { 352 | use super::*; 353 | 354 | const TEMPLATE: &str = r#"{pkgs}: { 355 | deps = [ 356 | pkgs.cowsay 357 | ]; 358 | } 359 | "#; 360 | 361 | #[test] 362 | fn test_integration_makes_template_if_missing() { 363 | let dir = tempfile::tempdir().unwrap(); 364 | let repl_nix_file = dir.path().join("replit.nix"); 365 | env::set_var("REPL_HOME", dir.path().display().to_string()); 366 | 367 | let args = Args { 368 | add: Some("pkgs.ncdu".to_string()), 369 | ..Default::default() 370 | }; 371 | real_main(&mut io::stdout(), args); 372 | 373 | let contents = fs::read_to_string(repl_nix_file.clone()).unwrap(); 374 | 375 | assert_eq!( 376 | r#"{pkgs}: { 377 | deps = [ 378 | pkgs.ncdu 379 | ]; 380 | } 381 | "#, 382 | contents 383 | ); 384 | 385 | drop(repl_nix_file); 386 | dir.close().unwrap(); 387 | } 388 | 389 | #[test] 390 | fn test_integration_makes_python_ld_library_if_missing() { 391 | let dir = tempfile::tempdir().unwrap(); 392 | let repl_nix_file = dir.path().join("replit.nix"); 393 | 394 | fs::write(repl_nix_file.as_os_str(), EMPTY_TEMPLATE.as_bytes()).unwrap(); 395 | 396 | let args = Args { 397 | path: Some(repl_nix_file.clone().display().to_string()), 398 | dep_type: DepType::Python, 399 | add: Some("pkgs.zlib".to_string()), 400 | ..Default::default() 401 | }; 402 | real_main(&mut io::stdout(), args); 403 | 404 | let contents = fs::read_to_string(repl_nix_file.clone()).unwrap(); 405 | 406 | assert_eq!( 407 | r#"{pkgs}: { 408 | deps = []; 409 | env = { 410 | PYTHON_LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ 411 | pkgs.zlib 412 | ]; 413 | }; 414 | } 415 | "#, 416 | contents 417 | ); 418 | drop(repl_nix_file); 419 | dir.close().unwrap(); 420 | } 421 | 422 | #[test] 423 | fn test_integration_no_change_no_write() { 424 | let dir = tempfile::tempdir().unwrap(); 425 | let repl_nix_file = dir.path().join("replit.nix"); 426 | 427 | fs::write(repl_nix_file.as_os_str(), EMPTY_TEMPLATE.as_bytes()).unwrap(); 428 | let args = Args { 429 | path: Some(repl_nix_file.clone().display().to_string()), 430 | dep_type: DepType::Python, 431 | add: Some("pkgs.zlib".to_string()), 432 | ..Default::default() 433 | }; 434 | real_main(&mut io::stdout(), args.clone()); 435 | 436 | let metadata = fs::metadata(repl_nix_file.as_os_str()).unwrap(); 437 | let modification_time = metadata.modified().unwrap(); 438 | 439 | real_main(&mut io::stdout(), args); 440 | 441 | let metadata = fs::metadata(repl_nix_file.as_os_str()).unwrap(); 442 | let modification_time2 = metadata.modified().unwrap(); 443 | 444 | assert_eq!(modification_time, modification_time2); 445 | } 446 | 447 | #[test] 448 | fn test_integration_remove_writes() { 449 | let dir = tempfile::tempdir().unwrap(); 450 | let repl_nix_file = dir.path().join("replit.nix"); 451 | 452 | fs::write(repl_nix_file.as_os_str(), TEMPLATE.as_bytes()).unwrap(); 453 | let args = Args { 454 | path: Some(repl_nix_file.clone().display().to_string()), 455 | dep_type: DepType::Regular, 456 | remove: Some("pkgs.cowsay".to_string()), 457 | ..Default::default() 458 | }; 459 | real_main(&mut io::stdout(), args.clone()); 460 | 461 | let contents = fs::read_to_string(repl_nix_file.clone()).unwrap(); 462 | 463 | assert_eq!("{pkgs}: {\n deps = [\n ];\n}\n", contents); 464 | 465 | drop(repl_nix_file); 466 | dir.close().unwrap(); 467 | } 468 | 469 | #[test] 470 | fn test_integration_get() { 471 | let dir = tempfile::tempdir().unwrap(); 472 | let repl_nix_file = dir.path().join("replit.nix"); 473 | 474 | fs::write(repl_nix_file.as_os_str(), TEMPLATE.as_bytes()).unwrap(); 475 | let args = Args { 476 | path: Some(repl_nix_file.clone().display().to_string()), 477 | get: true, 478 | ..Default::default() 479 | }; 480 | let mut stdout = Vec::new(); 481 | real_main(&mut stdout, args.clone()); 482 | 483 | assert_eq!( 484 | stdout, 485 | br#"{"status":"success","data":"pkgs.cowsay"} 486 | "# 487 | ); 488 | 489 | drop(repl_nix_file); 490 | dir.close().unwrap(); 491 | } 492 | } 493 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.58" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" 10 | 11 | [[package]] 12 | name = "atty" 13 | version = "0.2.14" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 16 | dependencies = [ 17 | "hermit-abi", 18 | "libc", 19 | "winapi", 20 | ] 21 | 22 | [[package]] 23 | name = "autocfg" 24 | version = "1.1.0" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 27 | 28 | [[package]] 29 | name = "bitflags" 30 | version = "1.3.2" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 33 | 34 | [[package]] 35 | name = "bitflags" 36 | version = "2.4.0" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" 39 | 40 | [[package]] 41 | name = "cc" 42 | version = "1.0.83" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 45 | dependencies = [ 46 | "libc", 47 | ] 48 | 49 | [[package]] 50 | name = "cfg-if" 51 | version = "1.0.0" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 54 | 55 | [[package]] 56 | name = "clap" 57 | version = "3.2.10" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "69c5a7f9997be616e47f0577ee38c91decb33392c5be4866494f34cdf329a9aa" 60 | dependencies = [ 61 | "atty", 62 | "bitflags 1.3.2", 63 | "clap_derive", 64 | "clap_lex", 65 | "indexmap", 66 | "once_cell", 67 | "strsim", 68 | "termcolor", 69 | "textwrap", 70 | ] 71 | 72 | [[package]] 73 | name = "clap_derive" 74 | version = "3.2.7" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902" 77 | dependencies = [ 78 | "heck", 79 | "proc-macro-error", 80 | "proc-macro2", 81 | "quote", 82 | "syn", 83 | ] 84 | 85 | [[package]] 86 | name = "clap_lex" 87 | version = "0.2.4" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" 90 | dependencies = [ 91 | "os_str_bytes", 92 | ] 93 | 94 | [[package]] 95 | name = "countme" 96 | version = "3.0.1" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" 99 | 100 | [[package]] 101 | name = "errno" 102 | version = "0.3.4" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480" 105 | dependencies = [ 106 | "errno-dragonfly", 107 | "libc", 108 | "windows-sys", 109 | ] 110 | 111 | [[package]] 112 | name = "errno-dragonfly" 113 | version = "0.1.2" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 116 | dependencies = [ 117 | "cc", 118 | "libc", 119 | ] 120 | 121 | [[package]] 122 | name = "fastrand" 123 | version = "2.0.1" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" 126 | 127 | [[package]] 128 | name = "hashbrown" 129 | version = "0.12.2" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022" 132 | 133 | [[package]] 134 | name = "heck" 135 | version = "0.4.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 138 | 139 | [[package]] 140 | name = "hermit-abi" 141 | version = "0.1.19" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 144 | dependencies = [ 145 | "libc", 146 | ] 147 | 148 | [[package]] 149 | name = "indexmap" 150 | version = "1.9.1" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" 153 | dependencies = [ 154 | "autocfg", 155 | "hashbrown", 156 | ] 157 | 158 | [[package]] 159 | name = "itoa" 160 | version = "1.0.2" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" 163 | 164 | [[package]] 165 | name = "libc" 166 | version = "0.2.148" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" 169 | 170 | [[package]] 171 | name = "linux-raw-sys" 172 | version = "0.4.8" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db" 175 | 176 | [[package]] 177 | name = "memoffset" 178 | version = "0.8.0" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" 181 | dependencies = [ 182 | "autocfg", 183 | ] 184 | 185 | [[package]] 186 | name = "nix-editor" 187 | version = "0.3.0" 188 | dependencies = [ 189 | "anyhow", 190 | "clap", 191 | "rnix", 192 | "rowan", 193 | "serde", 194 | "serde_json", 195 | "tempfile", 196 | ] 197 | 198 | [[package]] 199 | name = "once_cell" 200 | version = "1.13.0" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" 203 | 204 | [[package]] 205 | name = "os_str_bytes" 206 | version = "6.1.0" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" 209 | 210 | [[package]] 211 | name = "proc-macro-error" 212 | version = "1.0.4" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 215 | dependencies = [ 216 | "proc-macro-error-attr", 217 | "proc-macro2", 218 | "quote", 219 | "syn", 220 | "version_check", 221 | ] 222 | 223 | [[package]] 224 | name = "proc-macro-error-attr" 225 | version = "1.0.4" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 228 | dependencies = [ 229 | "proc-macro2", 230 | "quote", 231 | "version_check", 232 | ] 233 | 234 | [[package]] 235 | name = "proc-macro2" 236 | version = "1.0.40" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" 239 | dependencies = [ 240 | "unicode-ident", 241 | ] 242 | 243 | [[package]] 244 | name = "quote" 245 | version = "1.0.20" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" 248 | dependencies = [ 249 | "proc-macro2", 250 | ] 251 | 252 | [[package]] 253 | name = "redox_syscall" 254 | version = "0.3.5" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" 257 | dependencies = [ 258 | "bitflags 1.3.2", 259 | ] 260 | 261 | [[package]] 262 | name = "rnix" 263 | version = "0.11.0" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "bb35cedbeb70e0ccabef2a31bcff0aebd114f19566086300b8f42c725fc2cb5f" 266 | dependencies = [ 267 | "rowan", 268 | ] 269 | 270 | [[package]] 271 | name = "rowan" 272 | version = "0.15.11" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "64449cfef9483a475ed56ae30e2da5ee96448789fb2aa240a04beb6a055078bf" 275 | dependencies = [ 276 | "countme", 277 | "hashbrown", 278 | "memoffset", 279 | "rustc-hash", 280 | "text-size", 281 | ] 282 | 283 | [[package]] 284 | name = "rustc-hash" 285 | version = "1.1.0" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 288 | 289 | [[package]] 290 | name = "rustix" 291 | version = "0.38.19" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed" 294 | dependencies = [ 295 | "bitflags 2.4.0", 296 | "errno", 297 | "libc", 298 | "linux-raw-sys", 299 | "windows-sys", 300 | ] 301 | 302 | [[package]] 303 | name = "ryu" 304 | version = "1.0.10" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" 307 | 308 | [[package]] 309 | name = "serde" 310 | version = "1.0.138" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "1578c6245786b9d168c5447eeacfb96856573ca56c9d68fdcf394be134882a47" 313 | dependencies = [ 314 | "serde_derive", 315 | ] 316 | 317 | [[package]] 318 | name = "serde_derive" 319 | version = "1.0.138" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "023e9b1467aef8a10fb88f25611870ada9800ef7e22afce356bb0d2387b6f27c" 322 | dependencies = [ 323 | "proc-macro2", 324 | "quote", 325 | "syn", 326 | ] 327 | 328 | [[package]] 329 | name = "serde_json" 330 | version = "1.0.82" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" 333 | dependencies = [ 334 | "itoa", 335 | "ryu", 336 | "serde", 337 | ] 338 | 339 | [[package]] 340 | name = "strsim" 341 | version = "0.10.0" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 344 | 345 | [[package]] 346 | name = "syn" 347 | version = "1.0.98" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" 350 | dependencies = [ 351 | "proc-macro2", 352 | "quote", 353 | "unicode-ident", 354 | ] 355 | 356 | [[package]] 357 | name = "tempfile" 358 | version = "3.8.0" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" 361 | dependencies = [ 362 | "cfg-if", 363 | "fastrand", 364 | "redox_syscall", 365 | "rustix", 366 | "windows-sys", 367 | ] 368 | 369 | [[package]] 370 | name = "termcolor" 371 | version = "1.1.3" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 374 | dependencies = [ 375 | "winapi-util", 376 | ] 377 | 378 | [[package]] 379 | name = "text-size" 380 | version = "1.1.0" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "288cb548dbe72b652243ea797201f3d481a0609a967980fcc5b2315ea811560a" 383 | 384 | [[package]] 385 | name = "textwrap" 386 | version = "0.15.0" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" 389 | 390 | [[package]] 391 | name = "unicode-ident" 392 | version = "1.0.1" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" 395 | 396 | [[package]] 397 | name = "version_check" 398 | version = "0.9.4" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 401 | 402 | [[package]] 403 | name = "winapi" 404 | version = "0.3.9" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 407 | dependencies = [ 408 | "winapi-i686-pc-windows-gnu", 409 | "winapi-x86_64-pc-windows-gnu", 410 | ] 411 | 412 | [[package]] 413 | name = "winapi-i686-pc-windows-gnu" 414 | version = "0.4.0" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 417 | 418 | [[package]] 419 | name = "winapi-util" 420 | version = "0.1.5" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 423 | dependencies = [ 424 | "winapi", 425 | ] 426 | 427 | [[package]] 428 | name = "winapi-x86_64-pc-windows-gnu" 429 | version = "0.4.0" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 432 | 433 | [[package]] 434 | name = "windows-sys" 435 | version = "0.48.0" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 438 | dependencies = [ 439 | "windows-targets", 440 | ] 441 | 442 | [[package]] 443 | name = "windows-targets" 444 | version = "0.48.5" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 447 | dependencies = [ 448 | "windows_aarch64_gnullvm", 449 | "windows_aarch64_msvc", 450 | "windows_i686_gnu", 451 | "windows_i686_msvc", 452 | "windows_x86_64_gnu", 453 | "windows_x86_64_gnullvm", 454 | "windows_x86_64_msvc", 455 | ] 456 | 457 | [[package]] 458 | name = "windows_aarch64_gnullvm" 459 | version = "0.48.5" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 462 | 463 | [[package]] 464 | name = "windows_aarch64_msvc" 465 | version = "0.48.5" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 468 | 469 | [[package]] 470 | name = "windows_i686_gnu" 471 | version = "0.48.5" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 474 | 475 | [[package]] 476 | name = "windows_i686_msvc" 477 | version = "0.48.5" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 480 | 481 | [[package]] 482 | name = "windows_x86_64_gnu" 483 | version = "0.48.5" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 486 | 487 | [[package]] 488 | name = "windows_x86_64_gnullvm" 489 | version = "0.48.5" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 492 | 493 | [[package]] 494 | name = "windows_x86_64_msvc" 495 | version = "0.48.5" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 498 | --------------------------------------------------------------------------------