From 67bf612dbfd32ae8a5d848364cf1613db45935c7 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Fri, 28 Jun 2024 19:22:00 +0000 Subject: [PATCH] refactor: use generator helper for built-in secret generators --- --- a/Cargo.lock +++ b/Cargo.lock @@ -808,10 +808,12 @@ dependencies = [ "age", "anyhow", + "base64 0.22.1", "clap", "ed25519-dalek", "fleet-shared", "rand", + "x25519-dalek", ] [[package]] --- a/cmds/fleet/src/cmds/secrets/mod.rs +++ b/cmds/fleet/src/cmds/secrets/mod.rs @@ -40,9 +40,6 @@ /// Secret public part #[clap(long)] public: Option, - /// How to name public secret part - #[clap(long, default_value = "public")] - public_name: String, /// Load public part from specified file #[clap(long)] public_file: Option, @@ -55,14 +52,19 @@ #[clap(long)] re_add: bool, - #[clap(default_value = "secret")] - part_name: String, + /// How to name public secret part + #[clap(long, short = 'p', default_value = "public")] + public_part: String, + /// How to name private secret part + #[clap(short = 's', long, default_value = "secret")] + part: String, }, /// Add secret, data should be provided in stdin Add { /// Secret name name: String, - /// Secret owners + /// Secret owner + #[clap(short = 'm', long)] machine: String, /// Override secret if already present #[clap(long)] @@ -70,41 +72,41 @@ /// Secret public part #[clap(long)] public: Option, - /// How to name public secret part - #[clap(long, default_value = "public")] - public_name: String, /// Load public part from specified file #[clap(long)] public_file: Option, - #[clap(default_value = "secret")] - part_name: String, + /// How to name public secret part + #[clap(short = 'p', long, default_value = "public")] + public_part: String, + /// How to name private secret part + #[clap(short = 's', long, default_value = "secret")] + part: String, }, /// Read secret from remote host, requires sudo on said host Read { name: String, + #[clap(short = 'm', long)] machine: String, - #[clap(default_value = "secret")] - part_name: String, + /// Which private secret part to read + #[clap(short = 'p', long, default_value = "secret")] + part: String, }, UpdateShared { name: String, - #[clap(long)] - machines: Option>, + #[clap(short = 'm', long)] + machine: Option>, #[clap(long)] - add_machines: Vec, + add_machine: Vec, #[clap(long)] - remove_machines: Vec, + remove_machine: Vec, /// Which host should we use to decrypt #[clap(long)] prefer_identities: Vec, - - #[clap(default_value = "secret")] - part_name: String, }, Regenerate { /// Which host should we use to decrypt, in case if reencryption is required, without @@ -115,13 +117,15 @@ List {}, Edit { name: String, + #[clap(short = 'm', long)] machine: String, - - #[clap(default_value = "secret")] - part: String, #[clap(long)] add: bool, + + /// Which private secret part to read + #[clap(short = 'p', long, default_value = "secret")] + part: String, }, } @@ -220,21 +224,18 @@ }; let on_pkgs = host.pkgs().await?; let call_package = nix_go!(on_pkgs.callPackage); - let mk_encrypt_secret = nix_go!(on_pkgs.mkEncryptSecret); + let mk_secret_generators = nix_go!(on_pkgs.mkSecretGenerators); let mut recipients = Vec::new(); for owner in owners { let key = config.key(owner).await?; recipients.push(key); } - let encrypt = nix_go!(mk_encrypt_secret(Obj { + let generators = nix_go!(mk_secret_generators(Obj { recipients: { recipients }, })); - let generator = nix_go!(call_package(generator)(Obj { - encrypt, - // rustfmt_please_newline - })); + let generator = nix_go!(call_package(generator)(generators)); let generator = generator.build().await?; let generator = generator @@ -305,6 +306,7 @@ } let default_pkgs = &config.default_pkgs; let default_call_package = nix_go!(default_pkgs.callPackage); + let default_mk_secret_generators = nix_go!(default_pkgs.mkSecretGenerators); // Generators provide additional information in passthru, to access // passthru we should call generator, but information about where this generator is supposed to build // is located in passthru... Thus evaluating generator on host. @@ -313,10 +315,10 @@ // // I don't want to make modules always responsible for additional secret data anyway, // so it should be in derivation, and not in the secret data itself. - let default_generator = nix_go!(default_call_package(generator)(Obj { - encrypt: { "exit 1" }, - // rustfmt_please_newline + let generators = nix_go!(default_mk_secret_generators(Obj { + recipients: { >::new() }, })); + let default_generator = nix_go!(default_call_package(generator)(generators)); let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind); @@ -442,11 +444,11 @@ name, force, public, - public_name, + public_part: public_name, public_file, expires_at, re_add, - part_name, + part: part_name, } => { // TODO: Forbid updating secrets with set expectedOwners (= not user-managed). @@ -500,9 +502,9 @@ name, force, public, - public_name, + public_part: public_name, public_file, - part_name, + part: part_name, } => { if config.has_secret(&machine, &name) && !force { bail!("secret already defined"); @@ -535,7 +537,7 @@ Secret::Read { name, machine, - part_name, + part: part_name, } => { let secret = config.host_secret(&machine, &name)?; let Some(secret) = secret.parts.get(&part_name) else { @@ -552,25 +554,24 @@ } Secret::UpdateShared { name, - machines, - add_machines, - remove_machines, + machine, + add_machine, + remove_machine, prefer_identities, - part_name, } => { // TODO: Forbid updating secrets with set expectedOwners (= not user-managed). let secret = config.shared_secret(&name)?; - if secret.secret.parts.get(&part_name).is_none() { + if secret.secret.parts.values().all(|v| !v.raw.encrypted) { bail!("no secret"); } let initial_machines = secret.owners.clone(); let target_machines = parse_machines( initial_machines.clone(), - machines, - add_machines, - remove_machines, + machine, + add_machine, + remove_machine, )?; if target_machines.is_empty() { --- a/cmds/fleet/src/host.rs +++ b/cmds/fleet/src/host.rs @@ -95,7 +95,7 @@ let out = cmd.run_string().await?; let mut lines = out.split('\n'); if let Some(last) = lines.next_back() { - ensure!(last == "", "output of ls should end with newline"); + ensure!(last.is_empty(), "output of ls should end with newline"); } Ok(lines.map(ToOwned::to_owned).collect()) } --- a/cmds/generator-helper/Cargo.toml +++ b/cmds/generator-helper/Cargo.toml @@ -6,7 +6,9 @@ [dependencies] age.workspace = true anyhow.workspace = true +base64 = "0.22.1" clap.workspace = true ed25519-dalek = { version = "2.1", features = ["rand_core"] } fleet-shared.workspace = true rand = "0.8.5" +x25519-dalek = "2.0.1" --- a/cmds/generator-helper/src/main.rs +++ b/cmds/generator-helper/src/main.rs @@ -1,52 +1,161 @@ use std::{ - fs, - io::{self, stdout, Cursor, Read, Write}, - path::PathBuf, + env, + fs::{File, OpenOptions}, + io::{copy, Read, Write}, str::FromStr, }; -use age::Recipient; +use age::{ + ssh::{ParseRecipientKeyError, Recipient as SshRecipient}, + Encryptor, Recipient, +}; use anyhow::{anyhow, bail, ensure, Context, Result}; -use clap::Parser; -use ed25519_dalek::SigningKey; +use clap::{Parser, ValueEnum}; use fleet_shared::SecretData; use rand::{ distributions::{Alphanumeric, DistString, Distribution, Uniform}, - rngs::OsRng, - thread_rng, Rng, + thread_rng, }; -fn write_output(out: &str, data: impl AsRef<[u8]>, stdout_marker: &mut bool) -> Result<()> { - let data = data.as_ref(); - if out == "-" { - let mut stdout = stdout(); - if *stdout_marker { - stdout.write_all(&[b'\n'])?; +fn write_output_file(out: &str) -> Result { + let file = OpenOptions::new() + .create_new(true) + .write(true) + .open(out) + .with_context(|| format!("failed to open output {out:?}"))?; + Ok(file) +} +fn write_public(out: &str, mut input: impl Read, encoding: OutputEncoding) -> Result<()> { + let mut output = write_output_file(out)?; + + let mut data = Vec::new(); + copy(&mut input, &mut wrap_encoder(&mut data, encoding))?; + + output.write_all( + SecretData { + data, + encrypted: false, } - *stdout_marker = true; - stdout.write_all(data)?; - } else { - fs::write(out, data)?; + .to_string() + .as_bytes(), + )?; + Ok(()) +} +fn write_private( + identities: &Identities, + out: &str, + mut input: impl Read, + encoding: OutputEncoding, +) -> Result<()> { + let mut output = write_output_file(out)?; + let encryptor = make_encryptor(identities)?; + + let mut data = Vec::new(); + { + let mut encrypted_writer = encryptor.wrap_output(&mut data)?; + copy( + &mut input, + &mut wrap_encoder(&mut encrypted_writer, encoding), + )?; + encrypted_writer.finish()?; }; + + output.write_all( + SecretData { + data, + encrypted: true, + } + .to_string() + .as_bytes(), + )?; Ok(()) } +type Identities = Vec; +fn load_identities() -> Result { + let list = env::var("GENERATOR_HELPER_IDENTITIES"); + let list = match list { + Ok(v) => v, + Err(env::VarError::NotPresent) => { + bail!("gh is only intended to be used from secret generator scripts, but if you really want to use it somewhere else - set GENERATOR_HELPER_IDENTITIES to list of newline-delimited ssh identities"); + } + Err(e) => bail!("somehow, identities list is not utf-8: {e}"), + }; + let list = list.trim(); + ensure!(!list.is_empty(), "no identities passed, can't encrypt data"); + list.lines() + .map(age::ssh::Recipient::from_str) + .collect::>() + .map_err(|e| anyhow!("parse recipients: {e:?}")) +} +fn make_encryptor(r: &Identities) -> Result { + Ok(Encryptor::with_recipients( + r.iter() + .map(|v| { + let coerced: Box = Box::new(v.clone()); + coerced + }) + .collect(), + ) + .expect("list is not empty")) +} +fn wrap_encoder<'t>(w: impl Write + 't, encoding: OutputEncoding) -> impl Write + 't { + fn coerce<'t>(w: impl Write + 't) -> Box { + Box::new(w) + } + match encoding { + OutputEncoding::Raw => coerce(w), + OutputEncoding::Base64 => { + use base64::engine::general_purpose::STANDARD; + let writer = base64::write::EncoderWriter::new(w, &STANDARD); + coerce(writer) + } + } +} + +#[derive(Clone, Copy, ValueEnum, Default)] +enum OutputEncoding { + /// Do not encode data, store as is. + #[default] + Raw, + /// Encode as base64 (with padding). + Base64, +} + #[derive(Parser)] enum Generate { /// Generate public, private keys without wrapping, in standard ed25519 schema /// (64 bytes private (due to merge with private), 32 bytes public) Ed25519 { + #[arg(long, short = 'p')] public: String, + #[arg(long, short = 's')] private: String, /// Private key should be just the private key (32 bytes), not standard private+public. #[arg(long)] no_embed_public: bool, + #[arg(long, short = 'e', value_enum, default_value_t)] + encoding: OutputEncoding, + }, + /// Generate public, private keys without wrapping, in standard x25519 schema + /// (32 bytes private, 32 bytes public) + X25519 { + #[arg(long, short = 'p')] + public: String, + #[arg(long, short = 's')] + private: String, + #[arg(long, short = 'e', value_enum, default_value_t)] + encoding: OutputEncoding, }, Password { + #[arg(long, short = 'o')] output: String, + #[arg(long)] size: usize, #[arg(long, short = 'n')] no_symbols: bool, + #[arg(long, short = 'e', value_enum, default_value_t)] + encoding: OutputEncoding, }, } @@ -54,15 +163,17 @@ enum Opts { /// Encode public part from stdin. Public { - #[arg(long)] - allow_empty: bool, + #[arg(long, short = 'o')] + output: String, + #[arg(long, short = 'e', value_enum, default_value_t)] + encoding: OutputEncoding, }, /// Encrypt private part from stdin. Private { - #[arg(long)] - allow_empty: bool, - #[arg(short = 'r')] - recipient: Vec, + #[arg(long, short = 'o')] + output: String, + #[arg(long, short = 'e', value_enum, default_value_t)] + encoding: OutputEncoding, }, /// Generate keys in well-known schemas. /// @@ -70,99 +181,34 @@ /// otherwise you should ensure noone is able to read generated files, they don't have any mode set by default. #[command(subcommand)] Generate(Generate), - // Generate { - // kind: GenerateKind, - // /// Different generators generate different number of files, you need to specify number of outputs corresponding to the generator. - // #[arg(short = 'o')] - // outputs: Vec, - // }, } -fn parse_stdin() -> Result>> { - let mut input = vec![]; - io::stdin().read_to_end(&mut input)?; - if input.is_empty() { - Ok(None) - } else { - Ok(Some(input)) - } -} -pub fn encrypt_secret_data( - recipients: impl IntoIterator, - data: Vec, -) -> Option { - let mut encrypted = vec![]; - let recipients = recipients - .into_iter() - .map(|v| Box::new(v) as Box) - .collect::>(); - let mut encryptor = age::Encryptor::with_recipients(recipients)? - .wrap_output(&mut encrypted) - .expect("in memory write"); - io::copy(&mut Cursor::new(data), &mut encryptor).expect("in memory copy"); - encryptor.finish().expect("in memory flush"); - Some(SecretData { - data: encrypted, - encrypted: true, - }) -} - fn main() -> Result<()> { let opts = Opts::parse(); // Assumed to be secure, seeded from secure OsRng+reseeded. let mut rng = thread_rng(); match opts { - Opts::Public { allow_empty } => { - let stdin = parse_stdin()?; - if stdin.is_none() && !allow_empty { - bail!("empty stdin input is not allowed unless --allow-empty is set"); - } - let stdin = stdin.unwrap_or_default(); - io::stdout().write_all( - SecretData { - data: stdin, - encrypted: false, - } - .to_string() - .as_bytes(), - )?; + Opts::Public { output, encoding } => { + write_public(&output, std::io::stdin(), encoding)?; } - Opts::Private { - allow_empty, - recipient, - } => { - let stdin = parse_stdin()?; - if stdin.is_none() && !allow_empty { - bail!("empty stdin input is not allowed unless --allow-empty is set"); - } - let stdin = stdin.unwrap_or_default(); - if recipient.is_empty() { - bail!("recipient list is empty"); - } - let out = encrypt_secret_data( - recipient - .into_iter() - .map(|r| age::ssh::Recipient::from_str(&r)) - .collect::, age::ssh::ParseRecipientKeyError>>() - .map_err(|e| anyhow!("parse recipients: {e:?}"))?, - stdin, - ) - .expect("got recipients"); - io::stdout().write_all(out.to_string().as_bytes())?; + Opts::Private { output, encoding } => { + let recipients = load_identities()?; + write_private(&recipients, &output, std::io::stdin(), encoding)?; } Opts::Generate(gen) => { - let mut stdout_marker: bool = false; match gen { Generate::Ed25519 { public, private, no_embed_public, + encoding, } => { - let key = SigningKey::generate(&mut rng).to_keypair_bytes(); - - write_output(&public, &key[32..], &mut stdout_marker).context("public")?; - write_output( + let recipients = load_identities()?; + let key = ed25519_dalek::SigningKey::generate(&mut rng).to_keypair_bytes(); + write_public(&public, &key[32..], encoding)?; + write_private( + &recipients, &private, &key[..{ if no_embed_public { @@ -171,19 +217,31 @@ 64 } }], - &mut stdout_marker, - ) - .context("private")?; + encoding, + )?; + } + Generate::X25519 { + public, + private, + encoding, + } => { + let recipients = load_identities()?; + let key = x25519_dalek::StaticSecret::random_from_rng(rng); + let public_key: x25519_dalek::PublicKey = (&key).into(); + write_public(&public, public_key.as_bytes().as_slice(), encoding)?; + write_private(&recipients, &private, key.as_bytes().as_slice(), encoding)?; } Generate::Password { size, no_symbols, output, + encoding, } => { ensure!( size >= 6, "misconfiguration? password is shorter than 6 chars" ); + let recipients = load_identities()?; let out = if no_symbols { Alphanumeric.sample_string(&mut rng, size) } else { @@ -195,7 +253,7 @@ .map(|i| GEN_ASCII_SYMBOLS[i] as char) .collect::() }; - write_output(&output, out, &mut stdout_marker)?; + write_private(&recipients, &output, out.as_bytes(), encoding)?; } } } --- a/flake.nix +++ b/flake.nix @@ -67,6 +67,7 @@ perSystem = { config, system, + pkgs, ... }: let # Can also be built for darwin, through it is not usual to deploy nixos systems from macos machines. @@ -75,14 +76,14 @@ # It is not possible to deploy any host from armv6/armv7 hardware, and I don't think it even makes sense. deployerSystems = ["aarch64-linux" "x86_64-linux"]; deployerSystem = builtins.elem system deployerSystems; - pkgs = import nixpkgs { - inherit system; - overlays = [(rust-overlay.overlays.default)]; - }; lib = pkgs.lib; rust = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; craneLib = (crane.mkLib pkgs).overrideToolchain rust; in { + _module.args.pkgs = import nixpkgs { + inherit system; + overlays = [(rust-overlay.overlays.default)]; + }; # Reference fleet package should be built with nightly rust, specified in rust-toolchain.toml. packages = lib.mkIf deployerSystem (let packages = import ./pkgs { --- a/lib/fleetLib.nix +++ b/lib/fleetLib.nix @@ -42,23 +42,46 @@ mkPassword = {size ? 32}: { coreutils, - encrypt, mkSecretGenerator, + ... }: mkSecretGenerator { script = '' mkdir $out + gh generate password -o $out/secret --size ${toString size} + ''; + }; - ${coreutils}/bin/tr -dc 'A-Za-z0-9!?%=' < /dev/random \ - | ${coreutils}/bin/head -c ${toString size} \ - | ${encrypt} > $out/secret + mkEd25519 = { + noEmbedPublic ? false, + encoding ? null, + }: {mkSecretGenerator, ...}: + mkSecretGenerator { + script = '' + mkdir $out + gh generate ed25519 -p $out/public -s $out/secret \ + ${lib.optionalString noEmbedPublic "--no-embed-public"} \ + ${lib.optionalString (encoding != null) "--encoding=${encoding}"} ''; }; + mkGarage = {}: mkEd25519 {noEmbedPublic = true;}; + + mkX25519 = {encoding ? null}: {mkSecretGenerator, ...}: + mkSecretGenerator { + script = '' + mkdir $out + gh generate x25519 -p $out/public -s $out/secret \ + ${lib.optionalString (encoding != null) "--encoding=${encoding}"} + ''; + }; + + mkWireguard = {}: mkX25519 {encoding = "base64";}; + mkRsa = {size ? 4096}: { openssl, - encrypt, mkSecretGenerator, + ... }: mkSecretGenerator { script = '' @@ -67,8 +90,8 @@ ${openssl}/bin/openssl genrsa -out rsa_private.key ${toString size} ${openssl}/bin/openssl rsa -in rsa_private.key -pubout -out rsa_public.key - sudo cat rsa_private.key | ${encrypt} > $out/secret - sudo cat rsa_public.key > $out/public + cat rsa_private.key | gh private -o $out/secret + cat rsa_public.key | gh public -o $out/public ''; }; } --- a/modules/fleet/secrets.nix +++ b/modules/fleet/secrets.nix @@ -130,85 +130,81 @@ overlays = [ (final: prev: let lib = final.lib; - inherit (lib) strings concatMap; - inherit (strings) escapeShellArgs; + inherit (lib) strings; + inherit (strings) concatStringsSep; in { - mkEncryptSecret = { - rage ? prev.rage, - recipients, - }: - prev.writeShellScript "encryptor" '' - #!/bin/sh - exec ${rage}/bin/rage ${escapeShellArgs (concatMap (r: ["-r" r]) recipients)} -e "$@" - ''; - # TODO: Move to fleet - # TODO: Merge both generators to one with consistent options syntax? - # Impure generator is built on local machine, then built closure is copied to remote machine, - # and then it is ran in inpure context, so that this generator may access HSMs and other things. - mkImpureSecretGenerator = { - script, - # If set - script will be run on remote machine, otherwise it will be run with fleet project in CWD - # (Some secrets-encryption-in-git/managed PKI solution is expected) - impureOn ? null, - }: - (prev.writeShellScript "impureGenerator.sh" '' - #!/bin/sh - set -eu + mkSecretGenerators = {recipients}: rec { + # TODO: Merge both generators to one with consistent options syntax? + # Impure generator is built on local machine, then built closure is copied to remote machine, + # and then it is ran in inpure context, so that this generator may access HSMs and other things. + mkImpureSecretGenerator = { + script, + # If set - script will be run on remote machine, otherwise it will be run with fleet project in CWD + # (Some secrets-encryption-in-git/managed PKI solution is expected) + impureOn ? null, + }: + (prev.writeShellScript "impureGenerator.sh" '' + #!/bin/sh + set -eu + + export GENERATOR_HELPER_IDENTITIES="${concatStringsSep "\n" recipients}"; + export PATH=${final.fleet-generator-helper}/bin:$PATH - # TODO: Provide tempdir from outside, to make it securely erasurable as needed? - tmp=$(mktemp -d) - cd $tmp - # cd /var/empty + # TODO: Provide tempdir from outside, to make it securely erasurable as needed? + tmp=$(mktemp -d) + cd $tmp + # cd /var/empty - created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ") + created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ") - ${script} + ${script} - if ! test -d $out; then - echo "impure generator script did not produce expected \$out output" - exit 1 - fi + if ! test -d $out; then + echo "impure generator script did not produce expected \$out output" + exit 1 + fi - echo -n $created_at > $out/created_at - echo -n SUCCESS > $out/marker - '') - .overrideAttrs (old: { - passthru = { - inherit impureOn; - generatorKind = "impure"; - }; - }); - # Pure generators are disabled for now - mkSecretGenerator = {script}: final.mkImpureSecretGenerator {inherit script;}; + echo -n $created_at > $out/created_at + echo -n SUCCESS > $out/marker + '') + .overrideAttrs (old: { + passthru = { + inherit impureOn; + generatorKind = "impure"; + }; + }); + # Pure generators are disabled for now + mkSecretGenerator = {script}: mkImpureSecretGenerator {inherit script;}; - # TODO: Implement consistent naming - # Pure secret generator is supposed to be run entirely by nix, using `__impure` derivation type... - # But for now, it is ran the same way as `impureSecretGenerator`, but on the local machine. - # mkSecretGenerator = {script}: - # (prev.writeShellScript "generator.sh" '' - # #!/bin/sh - # set -eu - # # TODO: make nix daemon build secret, not just the script. - # cd /var/empty - # - # created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ") - # - # ${script} - # if ! test -d $out; then - # echo "impure generator script did not produce expected \$out output" - # exit 1 - # fi - # - # echo -n $created_at > $out/created_at - # echo -n SUCCESS > $out/marker - # '') - # .overrideAttrs (old: { - # passthru = { - # generatorKind = "pure"; - # }; - # # TODO: make nix daemon build secret, not just the script. - # # __impure = true; - # }); + # TODO: Implement consistent naming + # Pure secret generator is supposed to be run entirely by nix, using `__impure` derivation type... + # But for now, it is ran the same way as `impureSecretGenerator`, but on the local machine. + # mkSecretGenerator = {script}: + # (prev.writeShellScript "generator.sh" '' + # #!/bin/sh + # set -eu + # # TODO: make nix daemon build secret, not just the script. + # cd /var/empty + # + # created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ") + # + # ${script} + # if ! test -d $out; then + # echo "impure generator script did not produce expected \$out output" + # exit 1 + # fi + # + # echo -n $created_at > $out/created_at + # echo -n SUCCESS > $out/marker + # '') + # .overrideAttrs (old: { + # passthru = { + # generatorKind = "pure"; + # }; + # # TODO: make nix daemon build secret, not just the script. + # # __impure = true; + # }); + }; }) ]; }; --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -2,6 +2,7 @@ callPackage, craneLib, }: { + fleet = callPackage ./fleet.nix {inherit craneLib;}; fleet-install-secrets = callPackage ./fleet-install-secrets.nix {inherit craneLib;}; - fleet = callPackage ./fleet.nix {inherit craneLib;}; + fleet-generator-helper = callPackage ./fleet-generator-helper.nix {inherit craneLib;}; } --- /dev/null +++ b/pkgs/fleet-generator-helper.nix @@ -0,0 +1,13 @@ +{craneLib}: +craneLib.buildPackage rec { + pname = "fleet-generator-helper"; + + src = craneLib.cleanCargoSource (craneLib.path ../.); + strictDeps = true; + + cargoExtraArgs = "--locked -p ${pname}"; + + postInstall = '' + ln -s $out/bin/${pname} $out/bin/gh + ''; +} --- a/pkgs/generator-helper.nix +++ /dev/null @@ -1,13 +0,0 @@ -{craneLib}: -craneLib.buildPackage rec { - pname = "fleet-generator-helper"; - - src = craneLib.cleanCargoSource (craneLib.path ../.); - strictDeps = true; - - cargoExtraArgs = "--locked -p ${pname}"; - - postInstall = '' - mv bin/${pname} bin/genhelper - ''; -} -- gitstuff