diff --git a/devenv/src/devenv.rs b/devenv/src/devenv.rs index 3aa546737..5e8bbc2e4 100644 --- a/devenv/src/devenv.rs +++ b/devenv/src/devenv.rs @@ -650,15 +650,44 @@ impl Devenv { pub fn build(&mut self, attributes: &[String]) -> Result<()> { self.assemble(false)?; - let formatted_strings: Vec = attributes - .iter() - .map(|attr| format!("#.devenv.{}", attr)) - .collect(); - - let mut args: Vec<&str> = formatted_strings.iter().map(|s| s.as_str()).collect(); + let build_attrs: Vec = if attributes.is_empty() { + // construct dotted names of all attributes that we need to build + let build_output = self.run_nix( + "nix", + &["eval", ".#build", "--json"], + &command::Options::default(), + )?; + serde_json::from_slice::(&build_output.stdout) + .map_err(|e| miette::miette!("Failed to parse build output: {}", e))? + .as_object() + .ok_or_else(|| miette::miette!("Build output is not an object"))? + .iter() + .flat_map(|(key, value)| { + fn flatten_object(prefix: &str, value: &serde_json::Value) -> Vec { + match value { + serde_json::Value::Object(obj) => obj + .iter() + .flat_map(|(k, v)| flatten_object(&format!("{}.{}", prefix, k), v)) + .collect(), + _ => vec![format!(".#devenv.{}", prefix)], + } + } + flatten_object(key, value) + }) + .collect() + } else { + attributes + .iter() + .map(|attr| format!(".#devenv.{}", attr)) + .collect() + }; - args.insert(0, "build"); - self.run_nix("nix", &args, &command::Options::default())?; + let mut args = vec!["build", "--print-out-paths", "--no-link"]; + if !build_attrs.is_empty() { + args.extend(build_attrs.iter().map(|s| s.as_str())); + let output = self.run_nix("nix", &args, &command::Options::default())?; + println!("{}", String::from_utf8_lossy(&output.stdout)); + } Ok(()) } diff --git a/devenv/src/flake.tmpl.nix b/devenv/src/flake.tmpl.nix index bcc9f7f0b..6f6b051ca 100644 --- a/devenv/src/flake.tmpl.nix +++ b/devenv/src/flake.tmpl.nix @@ -100,14 +100,30 @@ v ); }; + build = options: config: + lib.concatMapAttrs + (name: option: + if builtins.hasAttr "type" option then + if option.type.name == "output" || option.type.name == "outputOf" then { + ${name} = config.${name}; + } else { } + else + let v = build option config.${name}; + in if v != { } then { + ${name} = v; + } else { } + ) + options; in { packages."${system}" = { optionsJSON = options.optionsJSON; + # deprecated inherit (config) info procfileScript procfileEnv procfile; ci = config.ciDerivation; }; devenv = config; + build = build project.options project.config; devShell."${system}" = config.shell; }; } diff --git a/docs/.pages b/docs/.pages index 245857bac..003f29a09 100644 --- a/docs/.pages +++ b/docs/.pages @@ -19,6 +19,7 @@ nav: - Containers: containers.md - Binary Caching: binary-caching.md - Pre-Commit Hooks: pre-commit-hooks.md + - Outputs: outputs.md - Tests: tests.md - Common Patterns: common-patterns.md - Writing devenv.yaml: diff --git a/docs/outputs.md b/docs/outputs.md new file mode 100644 index 000000000..f3212420c --- /dev/null +++ b/docs/outputs.md @@ -0,0 +1,71 @@ +# Outputs + +!!! info "New in version 1.1" + +Outputs allow you to define Nix derivations using the module system, +exposing Nix packages or sets of packages to be consumed by other tools for installation/distribution. + + +## Defining outputs + +You can define outputs in your `devenv.nix` file using the `outputs` attribute. Here's a simple example: + +```nix +{ pkgs, ... }: { + outputs = { + myproject.myapp = import ./myapp { inherit pkgs; }; + git = pkgs.git; + }; +} +``` + +In this example, we're defining two outputs: `myproject.myapp` and `git`. + +## Building outputs + +To build all defined outputs, run: + +```shell-session +$ devenv build +/nix/store/mzq5bpi49h26cy2mfj5a2r0q69fh3a9k-git-2.44.0 +/nix/store/mzq5bpi49h26cy2mfj5a2r0q69fh3a9k-myapp-1.0 +``` + +This command will build all outputs and display their paths in the Nix store. + +To build specific output(s), you can specify them explicitly: + +```shell-session +$ devenv build outputs.git +/nix/store/mzq5bpi49h26cy2mfj5a2r0q69fh3a9k-git-2.44.0 +``` + +This will build only the `git` output, making it easy to consume for installation or distribution. + +## Defining outputs as custom module options + +You can also define outputs using the module system's options. +This approach allows for more flexibility and integration with other parts of your configuration. + +Here's an example: + +```nix +{ pkgs, lib, config, ... }: { + options = { + myapp.package = pkgs.lib.mkOption { + type = config.lib.types.outputOf lib.types.package; + description = "The package for myapp"; + default = import ./myapp { inherit pkgs; }; + defaultText = "myapp"; + }; + }; + + config = { + outputs.git = pkgs.git; + } +} +``` + +In this case, `myapp.package` is defined as an output option. When building, devenv will automatically include this output along with any others defined in the `outputs` attribute. + +If you don't want to specify the output option type, you can use `config.lib.types.output` instead. diff --git a/src/modules/outputs.nix b/src/modules/outputs.nix new file mode 100644 index 000000000..819f4e4d3 --- /dev/null +++ b/src/modules/outputs.nix @@ -0,0 +1,36 @@ +{ pkgs, lib, config, ... }: { + options = { + outputs = lib.mkOption { + type = config.lib.types.outputOf lib.types.attrs; + default = { + git = pkgs.git; + foo = { + ncdu = pkgs.ncdu; + }; + }; + description = '' + Nix outputs for `devenv build` consumption. + ''; + }; + }; + + config.lib.types = { + output = lib.types.anything // { + name = "output"; + description = "output"; + descriptionClass = "output"; + }; + outputOf = t: lib.types.mkOptionType { + name = "outputOf"; + description = "outputOf ${lib.types.optionDescriptionPhrase (class: class == "noun" || class == "conjunction") t}"; + descriptionClass = "outputOf"; + check = t.check; + merge = t.merge; + emptyValue = t.emptyValue; + getSubOptions = t.getSubOptions; + getSubModules = t.getSubModules; + substSubModules = t.substSubModules; + nestedTypes.elemType = t; + }; + }; +} diff --git a/src/modules/top-level.nix b/src/modules/top-level.nix index edf4b361c..424e5e623 100644 --- a/src/modules/top-level.nix +++ b/src/modules/top-level.nix @@ -208,6 +208,7 @@ in imports = [ ./info.nix + ./outputs.nix ./processes.nix ./scripts.nix ./update-check.nix diff --git a/tests/outputs/devenv.nix b/tests/outputs/devenv.nix new file mode 100644 index 000000000..a4eb67da1 --- /dev/null +++ b/tests/outputs/devenv.nix @@ -0,0 +1,26 @@ +{ pkgs, lib, config, ... }: { + options = { + myapp.package = pkgs.lib.mkOption { + type = config.lib.types.outputOf lib.types.package; + description = "The package for myapp1"; + default = pkgs.writeText "myapp1" "touch $out"; + defaultText = "myapp1"; + }; + myapp2.package = pkgs.lib.mkOption { + type = config.lib.types.output; + description = "The package for myapp2"; + default = pkgs.writeText "myapp2" "touch $out"; + defaultText = "myapp2"; + }; + }; + config = { + enterTest = '' + devenv build | grep -E '(myapp1|git|myapp2|ncdu)' + devenv build myapp2.package | grep myapp2 + ''; + outputs = { + myproject.git = pkgs.git; + ncdu = pkgs.ncdu; + }; + }; +}