Skip to content
Merged
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
42 changes: 35 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,19 @@ cargo install anvil-env

Or build from source with `cargo build --release` and use `target/release/anvil`.

`cargo install` drops the binary in `~/.cargo/bin/anvil`. Add that directory to
`PATH` so wrappers, hooks, and project scripts can call `anvil` directly
instead of hard-coding the absolute path:

```bash
# bash / zsh
export PATH="$HOME/.cargo/bin:$PATH"
```

## Quick start

Write a package at `~/packages/maya-2024.yaml`:
Run `anvil init --config` to scaffold a starter `~/.anvil.yaml`, then write a
package at `~/packages/maya-2024.yaml`:

```yaml
name: maya
Expand Down Expand Up @@ -132,17 +142,30 @@ overwrite does not slip through.

The `commands` map lets `anvil run` pick a program from the package definition.
Values expand the same way as `environment` values, and can include baked in
arguments with whitespace or tilde segments:
arguments with whitespace or tilde segments — anvil tokenises the value with
POSIX shell rules, expands `~/` in every token, then runs the first token
with the remaining tokens prepended to whatever the user passes after `--`.

```yaml
commands:
# Bare path
maya: ${MAYA_LOCATION}/bin/maya

# Program + baked-in flags (multi-token alias)
nukex: ${NUKE_HOME}/Nuke${VERSION} --nukex

# Launcher in front of an interpreter (e.g. Python script with a specific runtime)
usdview: python3.14 ~/USD/bin/usdview

# Wrapper that injects defaults; user's `-- <extra args>` are appended
hython-debug: ${HFS}/bin/hython -d -v
```

Anvil tokenises the value with POSIX shell rules, expands `~/` in every token,
then runs the first token with the remaining tokens prepended to whatever the
user passes after `--`.
`anvil run nukex -- --view` therefore exec's
`${NUKE_HOME}/Nuke${VERSION} --nukex --view` — packages can ship sane defaults
for every tool they expose without forcing users to memorise flag soup. Quoted
substrings are preserved as a single argv element, so paths with spaces work
without escaping the whole value.

## Commands

Expand Down Expand Up @@ -234,12 +257,13 @@ anvil context shell render.ctx.json

### `anvil init`

Scaffold a new package definition.
Scaffold a new package definition, or a starter global config.

```bash
anvil init my-tools # my-tools/1.0.0/package.yaml
anvil init my-tools --version 2.0 # my-tools/2.0/package.yaml
anvil init my-tools --flat # my-tools-1.0.0.yaml
anvil init --config # ~/.anvil.yaml with a commented template
```

### `anvil completions`
Expand Down Expand Up @@ -363,7 +387,11 @@ filters:
|---|---|
| `ANVIL_CONFIG` | override config file location |
| `ANVIL_PACKAGES` | additional package paths, colon separated |
| `RUST_LOG` | log verbosity, e.g. `RUST_LOG=debug` |
| `RUST_LOG` | log verbosity, e.g. `RUST_LOG=debug` (overrides `-v`) |

By default anvil only logs warnings and errors so it can be piped safely
(`eval "$(anvil env maya-2024 --export)"`). Pass `-v` for info-level diagnostics
or `-vv` for debug.

If no config file is found, anvil falls back to `$ANVIL_PACKAGES`,
`$HOME/packages`, `$HOME/.local/share/anvil/packages`, and `/opt/packages`.
Expand Down
19 changes: 14 additions & 5 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ pub struct Cli {
#[arg(long, global = true)]
pub refresh: bool,

/// Increase log verbosity: `-v` enables info, `-vv` enables debug.
/// `RUST_LOG` overrides this when set.
#[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count, global = true)]
pub verbose: u8,

#[command(subcommand)]
pub command: Commands,
}
Expand Down Expand Up @@ -107,18 +112,22 @@ pub enum Commands {
action: ContextAction,
},

/// Scaffold a new package definition
/// Scaffold a new package definition (or `--config` for the global config)
Init {
/// Package name (e.g., my-tools)
name: String,
/// Package name (e.g., my-tools). Omit when using `--config`.
name: Option<String>,

/// Package version (default: 1.0.0)
#[arg(short, long, default_value = "1.0.0")]
#[arg(long, default_value = "1.0.0")]
version: String,

/// Create as a flat YAML file instead of a nested directory
#[arg(long)]
#[arg(long, conflicts_with = "config")]
flat: bool,

/// Scaffold a global `~/.anvil.yaml` instead of a package
#[arg(long, conflicts_with_all = ["flat", "name"])]
config: bool,
},

/// Generate shell completions
Expand Down
36 changes: 36 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,42 @@ impl Config {
Ok(config)
}

/// Diagnose why no packages were found and return a hint suitable for
/// printing to stderr. Returns `None` when at least one package path
/// exists on disk (i.e. there's no obvious config problem to surface).
pub fn first_run_hint(&self) -> Option<String> {
let config_path = Self::config_path();
if !config_path.exists() {
return Some(format!(
"No anvil config found at {}.\n - Run `anvil init --config` to scaffold one\n - Or set ANVIL_PACKAGES to a colon-separated list of package directories",
config_path.display()
));
}

if self.package_paths.is_empty() {
return Some(format!(
"{} sets no `package_paths`.\n - Add e.g. `package_paths: [~/packages]` to point anvil at your packages\n - Or set ANVIL_PACKAGES",
config_path.display()
));
}

if self.all_package_paths().is_empty() {
let listed = self
.package_paths
.iter()
.map(|p| format!(" - {}", p))
.collect::<Vec<_>>()
.join("\n");
return Some(format!(
"None of the configured package_paths exist on disk:\n{}\n Create one of them, or edit {}.",
listed,
config_path.display()
));
}

None
}

/// Get config file path
pub fn config_path() -> PathBuf {
if let Ok(path) = std::env::var("ANVIL_CONFIG") {
Expand Down
104 changes: 97 additions & 7 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,28 @@ use context::{ContextPackage, Lockfile, SavedContext};
use resolver::Resolver;

fn main() -> Result<()> {
// Initialize logging
let cli = Cli::parse();

// Default to WARN so casual `anvil env <pkg>` invocations don't litter
// stderr with "Loaded N packages" and similar. `-v` / `-vv` step up
// to info / debug; `RUST_LOG` still wins when set.
let default_filter = match cli.verbose {
0 => "anvil=warn",
1 => "anvil=info",
_ => "anvil=debug",
};
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "anvil=info".into()),
.unwrap_or_else(|_| default_filter.into()),
)
.with(
tracing_subscriber::fmt::layer()
.with_target(false)
.with_writer(std::io::stderr),
)
.with(tracing_subscriber::fmt::layer().with_target(false))
.init();

let cli = Cli::parse();

// Load config
let config = Config::load()?;
let refresh = cli.refresh;
Expand Down Expand Up @@ -71,8 +82,17 @@ fn main() -> Result<()> {
cmd_context_shell(&config, &file, shell)?;
}
},
Commands::Init { name, version, flat } => {
cmd_init(&name, &version, flat)?;
Commands::Init { name, version, flat, config: scaffold_config } => {
if scaffold_config {
cmd_init_config()?;
} else {
let name = name.ok_or_else(|| {
anyhow::anyhow!(
"anvil init: provide a package name, or pass --config to scaffold ~/.anvil.yaml"
)
})?;
cmd_init(&name, &version, flat)?;
}
}
Commands::Completions { shell } => {
Cli::print_completions(shell);
Expand Down Expand Up @@ -240,6 +260,21 @@ fn cmd_list(config: &Config, package: Option<String>, refresh: bool) -> Result<(
} else {
// List all packages
let packages = resolver.list_packages()?;
if packages.is_empty() {
if let Some(hint) = config.first_run_hint() {
eprintln!("No packages found.\n {}", hint.replace('\n', "\n "));
} else {
eprintln!(
"No packages found in any of the configured paths:\n{}",
config
.all_package_paths()
.iter()
.map(|p| format!(" - {}", p.display()))
.collect::<Vec<_>>()
.join("\n")
);
}
}
for pkg in packages {
println!("{}", pkg);
}
Expand All @@ -255,6 +290,15 @@ fn cmd_info(config: &Config, package: &str, refresh: bool) -> Result<()> {

println!("Name: {}", pkg.name);
println!("Version: {}", pkg.version);
// When the user asked for a bare name (e.g. `anvil info resolver`) and
// there are several versions on disk, surface them so the asymmetry
// between filename (`resolver-1.yaml`) and package name (`resolver`)
// doesn't hide the others.
if let Ok(versions) = resolver.list_versions(&pkg.name) {
if versions.len() > 1 {
println!("Available versions: {}", versions.join(", "));
}
}
if let Some(desc) = &pkg.description {
println!("Description: {}", desc);
}
Expand Down Expand Up @@ -524,6 +568,52 @@ environment:
Ok(())
}

/// Scaffold a global `~/.anvil.yaml` so first-time users have something to
/// edit instead of an empty file.
fn cmd_init_config() -> Result<()> {
let path = Config::config_path();
if path.exists() {
anyhow::bail!("{} already exists", path.display());
}

if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create {}", parent.display()))?;
}

let template = r#"# Anvil global config — see https://github.com/voidreamer/anvil

# Where to look for package definitions, in priority order.
# Each entry can be a directory of flat `<name>-<version>.yaml` files
# and/or nested `<name>/<version>/package.yaml` packages.
package_paths:
- ~/packages
# - /studio/packages
# - ${STUDIO_ROOT}/packages

# Optional: package set aliases (use as `anvil run <alias-name> -- ...`).
# aliases:
# maya-anim:
# - maya-2024
# - studio-tools

# Optional: shell that `anvil shell` uses by default.
# default_shell: zsh

# Optional: hide / restrict packages by glob.
# filters:
# include: ["maya-*", "houdini-*"]
# exclude: ["*-dev"]
"#;

std::fs::write(&path, template)
.with_context(|| format!("Failed to write {}", path.display()))?;
println!("Created {}", path.display());
println!("Edit it to point `package_paths` at your package directory, then run `anvil list`.");

Ok(())
}

// ---------------------------------------------------------------------------
// Wrap
// ---------------------------------------------------------------------------
Expand Down
Loading
Loading