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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions esp-template-sdk/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
42 changes: 37 additions & 5 deletions src/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ pub fn parse_template_arg(value: &str) -> Result<TemplateRef> {
/// 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,
}
Expand Down Expand Up @@ -107,24 +109,39 @@ pub fn clone(url: &str, reference: Option<&str>) -> Result<Checkout> {
}
}

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<PathBuf> {
let mut found = Vec::new();
Expand Down Expand Up @@ -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!(
Expand Down
152 changes: 139 additions & 13 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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<render::TemplateOrigin> {
if let Some(checkout) = checkout {
return Ok(render::TemplateOrigin::Clone {
url: checkout.url.clone(),
commit: checkout.commit.clone(),
Comment thread
MabezDev marked this conversation as resolved.
});
}

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<fetch::Checkout>)> {
Ok(match fetch::parse_template_arg(value)? {
Expand Down Expand Up @@ -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,
Expand All @@ -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();
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -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),
},
)?;

Expand Down Expand Up @@ -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 }
);
}
}
Loading
Loading