Skip to content

cli: init with template #1721

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion devenv/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,11 @@ impl GlobalOptions {
#[derive(Subcommand, Clone)]
pub enum Commands {
#[command(about = "Scaffold devenv.yaml, devenv.nix, .gitignore and .envrc.")]
Init { target: Option<PathBuf> },
Init {
target: Option<PathBuf>,
#[arg(long, help = "Use template")]
template: Option<String>,
},

#[command(about = "Generate devenv.yaml and devenv.nix using AI")]
Generate {
Expand Down
15 changes: 15 additions & 0 deletions devenv/src/cnix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,21 @@ impl Nix {

Ok(())
}

pub async fn flake_clone(&self, path: &PathBuf, flake_ref: &str) -> Result<()> {
let dest_path = path.to_str().unwrap();
self.run_nix("nix", &["flake", "clone", flake_ref, "--dest", dest_path], &self.options).await?;
Ok(())
}

pub async fn init(&self, path: &Path, flake_ref: &String) -> Result<()> {
let mut cmd =
self.prepare_command("nix", &["flake", "init", "--template", flake_ref], &self.options)?;
cmd.current_dir(path);
self.run_nix_command(cmd, &self.options).await?;

Ok(())
}

pub async fn metadata(&self) -> Result<String> {
let options = Options {
Expand Down
18 changes: 17 additions & 1 deletion devenv/src/devenv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ impl Devenv {
self.devenv_dotfile.join("processes.pid")
}

pub fn init(&self, target: &Option<PathBuf>) -> Result<()> {
pub async fn init(&mut self, target: &Option<PathBuf>, template: &Option<String>) -> Result<()> {
let target = target
.clone()
.unwrap_or_else(|| fs::canonicalize(".").expect("Failed to get current directory"));
Expand All @@ -154,6 +154,22 @@ impl Devenv {
std::fs::create_dir_all(&target).expect("Failed to create target directory");
}

if let Some(template) = template {
let temp_dir = self.devenv_runtime.join("clone");
self.nix.flake_clone(&temp_dir, template).await?;
let options = DevenvOptions {
devenv_root: Some(temp_dir.clone()),
..DevenvOptions::default()
};
let mut devenv = Devenv::new(options).await;
devenv.assemble(false).await?;
// Required, so that the directory is not viewed as a Git
// repository, otherwise the .devenv.flake.nix is ignored.
let flake_ref = format!("path:{}", &temp_dir.display());
self.nix.init(&target, &flake_ref).await?;
return Ok(());
}

for filename in REQUIRED_FILES {
info!("Creating {}", filename);

Expand Down
1 change: 1 addition & 0 deletions devenv/src/flake.tmpl.nix
Original file line number Diff line number Diff line change
Expand Up @@ -135,5 +135,6 @@
});
devenv = config;
build = build project.options project.config;
templates = config.templates;
};
}
2 changes: 1 addition & 1 deletion devenv/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ async fn main() -> Result<()> {
};
Ok(())
}
Commands::Init { target } => devenv.init(&target),
Commands::Init { target, template } => devenv.init(&target, &template).await,
Commands::Generate { .. } => match which::which("devenv-generate") {
Ok(devenv_generate) => {
let error = Command::new(devenv_generate)
Expand Down
1 change: 1 addition & 0 deletions docs/guides/.pages
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
nav:
- Using with Flakes: using-with-flakes.md
- Using with flake.parts: using-with-flake-parts.md
- Creating templates: creating-templates.md
92 changes: 92 additions & 0 deletions docs/guides/creating-templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
The following guide describes how to extend devenv and provide templates for other users. It is possible to bootstrap `devenv.nix` with custom content, change defaults or even define custom devenv options.

## Templates

To provide a devenv template, add the following to your `devenv.nix` file:
```nix title="devenv.nix"
templates.rust = {
path = ./rust;
description = "A simple Rust/Cargo project";
welcomeText = ''
# Simple Rust/Cargo Template
## Intended usage
The intended usage is...

## More info
- [Rust language](https://www.rust-lang.org/)
- [Rust on the NixOS Wiki](https://wiki.nixos.org/wiki/Rust)
- ...
'';
};

templates.default = config.templates.rust;
}
```

A template has the following attributes:
- `description`: A one-line description of the template, in CommonMark syntax.
- `path`: The path of the directory to be copied. This directory can be created with `devenv init rust`.
- `welcomeText`: A block of markdown text to display when a user initializes a new project based on this template.

Template consumers can then use the following command to use the `default` template:
```
devenv init --template github:owner/repo
```

The reference to the Github repository is called a Flake reference. Some more examples:
- `github:owner/repo#rust`: The `rust` template.
- `github:owner/repo/some-branch`: The `default` template from the `some-branch` Git branch.
- `gitlab:owner/repo`: The `default` template from a Gitlab repository.

When testing you may also use a local directory path.

!!! note
Devenv only supports a subset of the Flake reference specification. Notably it is currently not possible to specify a directory.

## Imports
Imports can be used to split `devenv.nix` when making more advanced templates.

```nix title="devenv.nix"
{ config, lib, ... }:
{
imports = [ ./another-file.nix ];
}
```

It is also possible to import from an external repository. This method is recommended for implementation details that the template consumer is not supposed to modify.

```yaml title="devenv.yaml"
inputs:
nixpkgs:
url: github:cachix/devenv-nixpkgs/rolling
templates:
url: github:owner/repo
imports:
- templates
```

The specified repository is usually the same repository where the template itself is located.

The import always begins with the input name, but may include a directory (e.g. `templates/some/directory`). In the example above the import will look for a `devenv.nix` file in the Git root directory.

!!! note "Defaults"
To set defaults prepend the value with `lib.mkDefault` so that the template consumer can change the value (e.g. `enable = lib.mkDefault true`). Lists are automatically merged and don't require `lib.mkDefault`. The template can specify a list of default packages and these are then merged with the users packages.

## Merging existing files

Templates cannot override existing files, however it is possible to implement custom logic to handle this.

First rename the template file (e.g. `.gitignore.devenv-template`) and then add the following to the `devenv.nix` file:

```nix
enterShell = ''
if [ -f .gitignore.devenv-template ]; then
cat .gitignore.devenv-template >> .gitignore
rm .gitignore.devenv-template
fi
'';
```

## Custom options

It is possible to extend devenv with additional modules and options. See <https://nix.dev/tutorials/module-system/deep-dive.html>
Loading