diff --git a/CHANGELOG.md b/CHANGELOG.md index 66ed9ff..c9ad212 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **`template_origin`, `template_repo`, `template_url` and `template_commit`.** A template can now see where it was read from. `template_origin` is `"clone"` when `--template` cloned a repository, which also sets `template_url` and `template_commit` to its URL and resolved commit, and `"repository"` when it named a local directory inside a git repository, which also sets `template_repo` to that repository's root, relative to the generated project and `/`-separated. Only the values for the origin at hand are set, and a directory outside any git checkout sets none, so a template that reads one that does not apply fails instead of rendering an empty string. A template shipped inside the project it scaffolds can depend on that project either way, branching on `template_origin`: `git = "{{ template_url }}", rev = "{{ template_commit }}"`, or `path = "{{ template_repo }}/my-crate"`. (#374) - **Templates that are not Cargo projects.** `metadata.toml` gained a `[project]` section: `type = "other"` tells the binary not to read a `Cargo.toml` — for the `esp-hal` version and the MSRV — and turns off the toolchain pre-flight, `cargo fmt` and the `Cargo.toml` reformat, leaving `git init`. Each of those is also settable on its own through `[project.steps]`, so a Rust template can drop one without dropping the rest. `type = "rust"` is the default, so existing templates are unaffected. (#372) - **`esp-generate check`** renders a template across its option combinations in memory and reports every failure with the selection that reaches it, without writing a project. This catches what generating once cannot: a name in a branch that selection never evaluates. `-o` pins an option (narrowing a required group instead of sweeping it), `--all-combinations` covers every valid union rather than each option once, `--exclude-group`/`--exclude-category`/`--cross-group` shape the matrix, and `--build` additionally generates each combination to a temporary directory and runs cargo check, clippy and fmt over it. It also warns when a template declares an older `sdk_version` than the features it uses require. (#369) - `--template` also accepts a **repository**: `owner/repo[@branch-or-tag]`, an `https://` URL, or `git@host:path`. It is cloned shallowly with `git` (reusing your existing credentials, so private template repos work) into a temporary directory that is removed when esp-generate exits, and the resolved commit is logged so a generated project can be traced back to exactly what produced it. The template need not be the repository root — esp-generate finds the directory holding a `metadata.toml` that parses as a manifest, so a template shipped inside a larger repo works. More than one is an error listing them, never a guess. A commit SHA is not supported: shallow clones cannot take one portably. (#368) diff --git a/esp-template-sdk/src/contract.rs b/esp-template-sdk/src/contract.rs index a0b52c6..94cddfe 100644 --- a/esp-template-sdk/src/contract.rs +++ b/esp-template-sdk/src/contract.rs @@ -185,6 +185,10 @@ mod test { "rust_toolchain", "reserved_gpio_code", "has_reserved_pins", + "template_origin", + "template_repo", + "template_url", + "template_commit", ] { assert!( feature(name).is_none(), diff --git a/src/fetch.rs b/src/fetch.rs index 4af96ed..986c28c 100644 --- a/src/fetch.rs +++ b/src/fetch.rs @@ -71,6 +71,8 @@ pub fn parse_template_arg(value: &str) -> Result { /// A cloned repository, deleted when this is dropped. pub struct Checkout { pub root: PathBuf, + /// In the form cargo accepts as a `git` dependency. + pub url: String, pub commit: String, _clone: TempDir, } @@ -107,24 +109,39 @@ pub fn clone(url: &str, reference: Option<&str>) -> Result { } } - let commit = Command::new("git") + let rev_parse = Command::new("git") .args(["rev-parse", "HEAD"]) .current_dir(path) .output() - .ok() - .filter(|o| o.status.success()) - .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) - .unwrap_or_else(|| "unknown".to_string()); + .context("could not run `git rev-parse` in the clone")?; + if !rev_parse.status.success() { + bail!( + "could not read the commit `{url}` was cloned at: {}", + String::from_utf8_lossy(&rev_parse.stderr).trim() + ); + } + let commit = String::from_utf8_lossy(&rev_parse.stdout) + .trim() + .to_string(); let root = find_template_root(path)?; Ok(Checkout { root, + url: cargo_url(url), commit, _clone: clone, }) } +/// Cargo rejects `scp`-style `git@host:path`, so it becomes `ssh://git@host/path`. +fn cargo_url(url: &str) -> String { + match url.split_once(':') { + Some((host, path)) if !url.contains("://") => format!("ssh://{host}/{path}"), + _ => url.to_string(), + } +} + /// Find the one directory in `dir` holding a template manifest. fn find_template_root(dir: &Path) -> Result { let mut found = Vec::new(); @@ -220,6 +237,21 @@ mod test { ); } + #[test] + fn a_template_reports_a_url_cargo_accepts() { + assert_eq!( + cargo_url("git@github.com:espressif/esp-hal.git"), + "ssh://git@github.com/espressif/esp-hal.git" + ); + for url in [ + "https://github.com/esp-rs/esp-hal", + "ssh://git@github.com/esp-rs/esp-hal", + "file:///tmp/esp-hal", + ] { + assert_eq!(cargo_url(url), url); + } + } + #[test] fn an_https_url_is_used_as_is() { assert_eq!( diff --git a/src/main.rs b/src/main.rs index e79ed53..fefaecf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use anyhow::{Result, bail}; +use anyhow::{Context, Result, bail}; use clap::{Parser, Subcommand}; use esp_generate::plugin; use esp_generate::sweep; @@ -146,7 +146,7 @@ enum SubCommands { } impl SubCommands { - fn handle(&self, loaded: &Loaded) -> Result<()> { + fn handle(&self, loaded: &Loaded, origin: &render::TemplateOrigin) -> Result<()> { fn compatibility_info_text(options: &[&GeneratorOption], opt: &GeneratorOption) -> String { // Collect every `compatible` group key used by any variant sharing // this option's name (there can be more than one variant — see the @@ -344,6 +344,7 @@ impl SubCommands { excluded_categories: exclude_category.clone(), crossed_groups: cross_group.clone(), }, + origin: origin.clone(), build: *build, dry_run: *dry_run, }, @@ -395,6 +396,36 @@ fn wants_interactive( user_chose_nothing && !headless } +/// Where the template a render sees was read from. +fn template_origin( + source: &TemplateSource, + checkout: Option<&fetch::Checkout>, +) -> Result { + if let Some(checkout) = checkout { + return Ok(render::TemplateOrigin::Clone { + url: checkout.url.clone(), + commit: checkout.commit.clone(), + }); + } + + let TemplateSource::Directory(dir) = source else { + return Ok(render::TemplateOrigin::Unknown); + }; + + let dir = fs::canonicalize(dir) + .with_context(|| format!("cannot resolve the template directory {}", dir.display()))?; + for ancestor in dir.ancestors() { + let git = ancestor.join(".git"); + if git + .try_exists() + .with_context(|| format!("cannot check for {}", git.display()))? + { + return Ok(render::TemplateOrigin::Repository(ancestor.to_path_buf())); + } + } + Ok(render::TemplateOrigin::Unknown) +} + /// Locate what a `--template` value names, cloning it first if it is remote. fn locate_template(value: &str) -> Result<(PathBuf, Option)> { Ok(match fetch::parse_template_arg(value)? { @@ -612,8 +643,9 @@ fn main() -> Result<()> { let mut args = Args::parse(); if let Some(subcommand) = args.subcommands.take() { - let (source, _checkout) = template_source(&args)?; - return subcommand.handle(&Loaded::open(source)?); + let (source, checkout) = template_source(&args)?; + let origin = template_origin(&source, checkout.as_ref())?; + return subcommand.handle(&Loaded::open(source)?, &origin); } // Only check for updates once the command-line arguments have been processed, @@ -625,7 +657,8 @@ fn main() -> Result<()> { } // Held for the whole run: a cloned template is deleted when this drops. - let (source, _checkout) = template_source(&args)?; + let (source, checkout) = template_source(&args)?; + let origin = template_origin(&source, checkout.as_ref())?; let loaded = Loaded::open(source)?; let user_chose_nothing = args.option.is_empty(); @@ -661,18 +694,23 @@ fn main() -> Result<()> { bail!("Directory already exists"); } + if let render::TemplateOrigin::Repository(repo) = &origin { + render::template_repo(repo, &path.join(&name))?; + } + let steps = loaded.manifest.steps(); - let (esp_hal_version_full, msrv) = if loaded.manifest.is_cargo_project() { - let versions = cargo::CargoToml::load( - loaded - .source - .get("Cargo.toml") - .ok_or_else(|| anyhow::anyhow!("template has no `Cargo.toml`"))? - .as_ref(), - ) + // A template may emit its manifest from a differently-named file, so an + // absent `Cargo.toml` is not an error — there is simply nothing to read. + let template_manifest = loaded + .source + .get("Cargo.toml") + .filter(|_| loaded.manifest.is_cargo_project()) + .map(|raw| cargo::CargoToml::load(raw.as_ref())) + .transpose() .map_err(|e| anyhow::anyhow!("template `Cargo.toml` is unreadable: {e}"))?; + let (esp_hal_version_full, msrv) = if let Some(versions) = template_manifest { // TODO: do not assume esp-hal version is present let esp_hal_version_full = render::esp_hal_version_full(&versions.dependency_version("esp-hal")); @@ -930,6 +968,8 @@ fn main() -> Result<()> { generate_parameters: selected_options, esp_hal_version_full, rust_toolchain: selected_toolchain.clone(), + origin, + project_dir: path.join(&name), }, )?; @@ -1524,4 +1564,90 @@ mod test { assert!(!requires_nightly(&ssp, &flat, true), "Xtensa is exempt"); } + + fn git(dir: &Path, args: &[&str]) -> String { + let output = std::process::Command::new("git") + .args(["-c", "user.name=t", "-c", "user.email=t@t"]) + .args(["-c", "commit.gpgsign=false"]) + .args(args) + .current_dir(dir) + .output() + .unwrap(); + assert!(output.status.success(), "git {args:?} failed"); + String::from_utf8_lossy(&output.stdout).trim().to_string() + } + + fn origin_test_dir() -> tempfile::TempDir { + tempfile::Builder::new() + .prefix("esp-generate-origin-test-") + .tempdir() + .unwrap() + } + + #[test] + fn the_bundled_template_has_no_origin() { + assert_eq!( + template_origin(&TemplateSource::Bundled, None).unwrap(), + render::TemplateOrigin::Unknown + ); + } + + #[test] + fn a_directory_inside_a_git_repository_reports_its_root() { + let dir = origin_test_dir(); + git(dir.path(), &["init", "-q"]); + let template = dir.path().join("templates/basic"); + fs::create_dir_all(&template).unwrap(); + + assert_eq!( + template_origin(&TemplateSource::Directory(template), None).unwrap(), + render::TemplateOrigin::Repository(fs::canonicalize(dir.path()).unwrap()) + ); + } + + #[test] + fn a_directory_outside_git_has_no_origin() { + let dir = origin_test_dir(); + assert_eq!( + template_origin(&TemplateSource::Directory(dir.path().into()), None).unwrap(), + render::TemplateOrigin::Unknown + ); + } + + #[test] + fn a_directory_that_cannot_be_resolved_is_an_error() { + let dir = origin_test_dir(); + let missing = TemplateSource::Directory(dir.path().join("missing")); + assert!(template_origin(&missing, None).is_err()); + } + + #[test] + fn a_clone_reports_its_url_and_commit() { + let dir = origin_test_dir(); + git(dir.path(), &["init", "-q"]); + fs::write( + dir.path().join("metadata.toml"), + "sdk_version = \"0.1.0\"\n", + ) + .unwrap(); + git(dir.path(), &["add", "-A"]); + git( + dir.path(), + &["commit", "-q", "--no-verify", "-m", "template"], + ); + let commit = git(dir.path(), &["rev-parse", "HEAD"]); + + let path = dir.path().display().to_string().replace('\\', "/"); + let url = format!( + "file://{}{path}", + if path.starts_with('/') { "" } else { "/" } + ); + let checkout = fetch::clone(&url, None).unwrap(); + let source = TemplateSource::Directory(checkout.root.clone()); + + assert_eq!( + template_origin(&source, Some(&checkout)).unwrap(), + render::TemplateOrigin::Clone { url, commit } + ); + } } diff --git a/src/render.rs b/src/render.rs index 0bb1bed..37be516 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,6 +1,8 @@ //! Turning a selection into the files a project would contain. Generation //! writes the result; `check` discards it and keeps the errors. +use std::path::{Path, PathBuf}; + use anyhow::{Result, bail}; use esp_generate::Loaded; @@ -10,6 +12,17 @@ use esp_generate::plugin::selection; use esp_generate::process; use esp_generate::template::GeneratorOption; +/// Where the template was read from. +#[derive(Clone, Debug, PartialEq)] +pub enum TemplateOrigin { + /// A directory inside the git repository rooted here, canonicalized. + Repository(PathBuf), + /// A clone of `url` at `commit`. + Clone { url: String, commit: String }, + /// The bundled template, or a directory outside any git repository. + Unknown, +} + /// The values the host supplies to every render. pub struct HostValues { pub project_name: String, @@ -20,6 +33,78 @@ pub struct HostValues { pub esp_hal_version_full: Option, /// `None` when the template declares no toolchain-bearing target. pub rust_toolchain: Option, + pub origin: TemplateOrigin, + /// Where the project is, or would be, written; need not exist. Read only + /// for a [`TemplateOrigin::Repository`]. + pub project_dir: PathBuf, +} + +const ORIGIN_VALUES: [&str; 4] = [ + "template_origin", + "template_repo", + "template_url", + "template_commit", +]; + +/// Why `error` names an origin value that was not set, if it does. +fn origin_hint(facts: &process::Facts, error: &str) -> Option { + let value = ORIGIN_VALUES + .iter() + .find(|v| error.contains(**v) && !facts.values.contains_key(**v))?; + Some(match facts.values.get("template_origin") { + Some(process::FactValue::Str(origin)) => format!( + "`{value}` is not set when `template_origin` is \"{origin}\"; branch on \ + `template_origin` before using it." + ), + _ => format!( + "`{value}` is unavailable: the template directory is not inside a git checkout, \ + and was not cloned by `--template`." + ), + }) +} + +/// `to` as a `/`-separated path relative to `from`, both canonical. +fn relative_path(from: &Path, to: &Path) -> Result { + let from: Vec<_> = from.components().collect(); + let to: Vec<_> = to.components().collect(); + if from.first() != to.first() { + bail!( + "the project at {} and the template's repository at {} are on different drives, \ + so the project cannot refer to the repository by a relative path. Generate it on \ + the same drive as the repository. `check` places its project in the current \ + directory, or with `--build` in the temporary directory, which `TMP` moves.", + from.iter().collect::().display(), + to.iter().collect::().display(), + ); + } + + let common = from.iter().zip(&to).take_while(|(a, b)| a == b).count(); + let mut parts = vec![".."; from.len() - common]; + for component in &to[common..] { + match component.as_os_str().to_str() { + Some(part) if !part.contains(['"', '\\']) => parts.push(part), + _ => bail!( + "the template's repository at {} cannot be written into a project manifest: \ + its path must be UTF-8 and contain neither `\"` nor `\\`.", + to.iter().collect::().display(), + ), + } + } + Ok(if parts.is_empty() { + ".".to_string() + } else { + parts.join("/") + }) +} + +/// `template_repo`: the repository root `repo`, relative to the project. +pub fn template_repo(repo: &Path, project_dir: &Path) -> Result { + let (Some(parent), Some(name)) = (project_dir.parent(), project_dir.file_name()) else { + bail!("`{}` is not a project directory", project_dir.display()); + }; + let parent = std::fs::canonicalize(parent) + .map_err(|e| anyhow::anyhow!("cannot resolve {}: {e}", parent.display()))?; + relative_path(&parent.join(name), repo) } /// The template's `esp-hal` version, padded to the `x.y.z` docs.rs links need. @@ -88,6 +173,18 @@ pub fn facts( facts.set_value("generate_version", env!("CARGO_PKG_VERSION")); facts.set_value("project_name", host.project_name.clone()); facts.set_value("generate_parameters", host.generate_parameters.clone()); + match &host.origin { + TemplateOrigin::Repository(repo) => { + facts.set_value("template_origin", "repository"); + facts.set_value("template_repo", template_repo(repo, &host.project_dir)?); + } + TemplateOrigin::Clone { url, commit } => { + facts.set_value("template_origin", "clone"); + facts.set_value("template_url", url.clone()); + facts.set_value("template_commit", commit.clone()); + } + TemplateOrigin::Unknown => {} + } if let Some(version) = &host.esp_hal_version_full { facts.set_value("esp_hal_version_full", version.clone()); } @@ -125,6 +222,14 @@ fn selected_groups(selected: &[String], flat_options: &[GeneratorOption]) -> Res Ok(groups) } +/// What `cargo fmt` is given on top of any `rustfmt.toml` it finds. +pub const RUSTFMT_CONFIG: [&str; 4] = [ + "--config", + "group_imports=StdExternalCrate", + "--config", + "imports_granularity=Module", +]; + /// Format a freshly written project the way generation does. /// /// `check --build` runs `cargo fmt --check` over the result, so it has to see @@ -132,14 +237,8 @@ fn selected_groups(selected: &[String], flat_options: &[GeneratorOption]) -> Res pub fn format_project(steps: &manifest::Steps, project_dir: &std::path::Path) -> Result<()> { if steps.cargo_fmt { std::process::Command::new("cargo") - .args([ - "fmt", - "--", - "--config", - "group_imports=StdExternalCrate", - "--config", - "imports_granularity=Module", - ]) + .args(["fmt", "--"]) + .args(RUSTFMT_CONFIG) .current_dir(project_dir) .output()?; } @@ -191,9 +290,13 @@ pub fn plan( } } - let processed = renderer - .render(&contents, &mut load_partial) - .map_err(|e| anyhow::anyhow!("{source_path}:{e}"))?; + let processed = + renderer.render(&contents, &mut load_partial).map_err(|e| { + match origin_hint(facts, &e.to_string()) { + Some(hint) => anyhow::anyhow!("{source_path}:{e}\n{hint}"), + None => anyhow::anyhow!("{source_path}:{e}"), + } + })?; let out_path = match output { Some(path) => { @@ -226,6 +329,182 @@ mod test { /// Unformatted on purpose: both steps would rewrite it. const RAGGED: &str = "[package]\nname=\"x\"\nversion=\"0.1.0\"\n"; + #[test] + fn a_directory_and_a_clone_report_disjoint_origins() { + let dir = tempfile::Builder::new() + .prefix("esp-generate-origin-test-") + .tempdir() + .unwrap(); + std::fs::write( + dir.path().join("metadata.toml"), + "sdk_version = \"0.1.0\"\n[project]\ntype = \"other\"\n", + ) + .unwrap(); + std::fs::write( + dir.path().join("template.yaml"), + "options:\n - !Option\n name: pinned\n display_name: Pinned\n", + ) + .unwrap(); + + let loaded = + esp_generate::Loaded::open(esp_generate::TemplateSource::Directory(dir.path().into())) + .expect("fixture must load"); + let flat = esp_generate::config::flatten_options(&loaded.template.options); + + let root = std::fs::canonicalize(dir.path()).unwrap(); + std::fs::create_dir_all(root.join("esp-hal")).unwrap(); + std::fs::create_dir_all(root.join("work/repro")).unwrap(); + + let host = |origin, project_dir| HostValues { + project_name: "p".to_string(), + generate_parameters: String::new(), + esp_hal_version_full: None, + rust_toolchain: None, + origin, + project_dir, + }; + let value = |facts: &process::Facts, k: &str| match facts.values.get(k) { + Some(process::FactValue::Str(s)) => s.to_string(), + other => panic!("`{k}` is {other:?}"), + }; + + let absent = |facts: &process::Facts, k: &str| { + assert!(!facts.values.contains_key(k), "`{k}` is set"); + }; + + let (local, _) = facts( + &loaded, + &[], + &flat, + &host( + TemplateOrigin::Repository(root.join("esp-hal")), + root.join("work/repro/p"), + ), + ) + .unwrap(); + assert_eq!(value(&local, "template_origin"), "repository"); + assert_eq!(value(&local, "template_repo"), "../../../esp-hal"); + absent(&local, "template_url"); + absent(&local, "template_commit"); + + let (inside, _) = facts( + &loaded, + &[], + &flat, + &host( + TemplateOrigin::Repository(root.join("esp-hal")), + root.join("esp-hal/p"), + ), + ) + .unwrap(); + assert_eq!(value(&inside, "template_repo"), ".."); + + let (cloned, _) = facts( + &loaded, + &[], + &flat, + &host( + TemplateOrigin::Clone { + url: "https://github.com/esp-rs/esp-hal".to_string(), + commit: "c4e5ba70c".to_string(), + }, + root.join("work/repro/p"), + ), + ) + .unwrap(); + assert_eq!(value(&cloned, "template_origin"), "clone"); + assert_eq!( + value(&cloned, "template_url"), + "https://github.com/esp-rs/esp-hal" + ); + assert_eq!(value(&cloned, "template_commit"), "c4e5ba70c"); + absent(&cloned, "template_repo"); + } + + #[test] + fn reading_an_unset_origin_value_fails_with_the_reason() { + let dir = tempfile::Builder::new() + .prefix("esp-generate-origin-test-") + .tempdir() + .unwrap(); + std::fs::write( + dir.path().join("metadata.toml"), + "sdk_version = \"0.1.0\"\n[project]\ntype = \"other\"\n", + ) + .unwrap(); + std::fs::write(dir.path().join("template.yaml"), "options: []\n").unwrap(); + std::fs::write(dir.path().join("deps.txt"), "{{ template_repo }}\n").unwrap(); + + let loaded = + esp_generate::Loaded::open(esp_generate::TemplateSource::Directory(dir.path().into())) + .expect("fixture must load"); + let host = |origin| HostValues { + project_name: "p".to_string(), + generate_parameters: String::new(), + esp_hal_version_full: None, + rust_toolchain: None, + origin, + project_dir: dir.path().join("p"), + }; + + let (unknown, _) = facts(&loaded, &[], &[], &host(TemplateOrigin::Unknown)).unwrap(); + for value in ORIGIN_VALUES { + assert!( + !unknown.values.contains_key(value), + "`{value}` claims an origin" + ); + } + let err = plan(&loaded, &[], &[], &unknown) + .err() + .expect("reading `template_repo` must fail"); + assert!( + err.to_string().contains("not inside a git checkout"), + "{err}" + ); + + let clone = TemplateOrigin::Clone { + url: "https://github.com/esp-rs/esp-hal".to_string(), + commit: "c4e5ba70c".to_string(), + }; + let (cloned, _) = facts(&loaded, &[], &[], &host(clone)).unwrap(); + let err = plan(&loaded, &[], &[], &cloned) + .err() + .expect("a clone has no `template_repo`"); + assert!( + err.to_string().contains("branch on `template_origin`"), + "{err}" + ); + } + + #[test] + fn a_repository_path_that_cannot_be_written_into_toml_is_refused() { + let err = relative_path(Path::new("/w/p"), Path::new("/w/a\"b")).unwrap_err(); + assert!(err.to_string().contains("cannot be written"), "{err}"); + + assert_eq!( + relative_path(Path::new("/a\"b/p"), Path::new("/a\"b/esp-hal")).unwrap(), + "../esp-hal", + "only the part that reaches the manifest has to be writable" + ); + } + + #[cfg(unix)] + #[test] + fn a_backslash_or_non_utf8_repository_path_is_refused() { + use std::os::unix::ffi::OsStrExt; + + assert!(relative_path(Path::new("/w/p"), Path::new("/w/a\\b")).is_err()); + let non_utf8 = Path::new("/w").join(std::ffi::OsStr::from_bytes(b"\xff")); + assert!(relative_path(Path::new("/w/p"), &non_utf8).is_err()); + } + + #[cfg(windows)] + #[test] + fn a_repository_on_another_drive_is_refused() { + let err = relative_path(Path::new(r"C:\work\p"), Path::new(r"D:\esp-hal")).unwrap_err(); + assert!(err.to_string().contains("different drives"), "{err}"); + } + #[test] fn a_template_that_opted_out_gets_neither_formatter() { let dir = tempfile::Builder::new() diff --git a/src/validate.rs b/src/validate.rs index cfccb87..678481d 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -2,7 +2,7 @@ //! what breaks, without writing anything. use std::fmt::Write as _; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::Command; use anyhow::{Result, bail}; @@ -24,6 +24,7 @@ struct Failure { /// How much of the template to exercise. pub struct Request { pub sweep: SweepOptions, + pub origin: render::TemplateOrigin, /// Also generate each combination to a temporary directory and run cargo /// over it. pub build: bool, @@ -57,21 +58,14 @@ pub fn run(loaded: &Loaded, request: &Request) -> Result<()> { ); let flat_options = flatten_options(&loaded.template.options); - let esp_hal_version = if loaded.manifest.is_cargo_project() { - Some(render::esp_hal_version_full( - &crate::cargo::CargoToml::load( - loaded - .source - .get("Cargo.toml") - .ok_or_else(|| anyhow::anyhow!("template has no `Cargo.toml`"))? - .as_ref(), - ) - .map_err(|e| anyhow::anyhow!("template `Cargo.toml` is unreadable: {e}"))? - .dependency_version("esp-hal"), - )) - } else { - None - }; + let esp_hal_version = loaded + .source + .get("Cargo.toml") + .filter(|_| loaded.manifest.is_cargo_project()) + .map(|raw| crate::cargo::CargoToml::load(raw.as_ref())) + .transpose() + .map_err(|e| anyhow::anyhow!("template `Cargo.toml` is unreadable: {e}"))? + .map(|versions| render::esp_hal_version_full(&versions.dependency_version("esp-hal"))); let mut failures = Vec::new(); let mut predicates_used: Vec<&'static str> = Vec::new(); @@ -81,6 +75,7 @@ pub fn run(loaded: &Loaded, request: &Request) -> Result<()> { combination, &flat_options, esp_hal_version.as_deref(), + &request.origin, request.build, ) { Ok(used) => { @@ -145,8 +140,22 @@ fn check_one( selected: &[String], flat_options: &[GeneratorOption], esp_hal_version: Option<&str>, + origin: &render::TemplateOrigin, build: bool, ) -> Result> { + let dir = build + .then(|| { + tempfile::Builder::new() + .prefix("esp-generate-check-") + .tempdir() + }) + .transpose()?; + let project = match (&dir, origin) { + (Some(dir), _) => dir.path().join("check"), + (None, render::TemplateOrigin::Repository(_)) => std::env::current_dir()?.join("check"), + (None, _) => PathBuf::from("check"), + }; + let (facts, _target) = render::facts( loaded, selected, @@ -160,6 +169,8 @@ fn check_one( .join(" "), esp_hal_version_full: esp_hal_version.map(str::to_string), rust_toolchain: None, + origin: origin.clone(), + project_dir: project.clone(), }, )?; @@ -169,10 +180,6 @@ fn check_one( return Ok(planned.predicates_used); } - let dir = tempfile::Builder::new() - .prefix("esp-generate-check-") - .tempdir()?; - let project = dir.path().join("check"); for (out_path, contents) in planned.files { let out_path = project.join(out_path); std::fs::create_dir_all(out_path.parent().unwrap())?; @@ -187,7 +194,10 @@ fn check_one( } cargo(&project, &["clippy", "--no-deps", "--", "-Dwarnings"])?; if steps.cargo_fmt { - cargo(&project, &["fmt", "--", "--check"])?; + cargo( + &project, + &[&["fmt", "--", "--check"][..], &render::RUSTFMT_CONFIG].concat(), + )?; } Ok(planned.predicates_used) @@ -250,6 +260,7 @@ options: fn request() -> Request { Request { sweep: SweepOptions::default(), + origin: render::TemplateOrigin::Unknown, build: false, dry_run: false, }