From f717c279506e8761b8d8cc56bb619b6ad4692c21 Mon Sep 17 00:00:00 2001 From: Alex Kiernan Date: Sun, 31 May 2026 12:07:30 +0100 Subject: [PATCH 1/5] Treat missing homepage/repository as a warning, not an error HOMEPAGE is optional in BitBake. Workspace members often have neither field set, causing cargo-bitbake to abort. Demote to a warning and emit an empty HOMEPAGE instead. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Alex Kiernan --- src/main.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/main.rs b/src/main.rs index 07a96c7..17e20de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -334,20 +334,19 @@ fn real_main(options: Args, gctx: &mut GlobalContext) -> CliResult { |s| cargo::util::interning::InternedString::new(&s.trim().replace("\n", " \\\n")), ); - // package homepage (or source code location) + // package homepage (or source code location); HOMEPAGE is optional in + // BitBake so missing metadata is a warning, not an error let homepage = metadata .homepage - .as_ref() - .map_or_else( - || { - println!("No package.homepage set in your Cargo.toml, trying package.repository"); - metadata - .repository - .as_ref() - .ok_or_else(|| anyhow!("No package.repository set in your Cargo.toml")) - }, - Ok, - )? + .as_deref() + .or_else(|| { + println!("No package.homepage set in your Cargo.toml, trying package.repository"); + metadata.repository.as_deref() + }) + .unwrap_or_else(|| { + println!("No package.repository set in your Cargo.toml, HOMEPAGE will be empty"); + "" + }) .trim(); // package license From 956cdb2c59ca811c19e4933956ce9744ff3f6ff4 Mon Sep 17 00:00:00 2001 From: Alex Kiernan Date: Sun, 31 May 2026 06:58:17 +0100 Subject: [PATCH 2/5] Anchor CARGO_SRC_DIR and LIC_FILES_CHKSUM at the git checkout root The git checkout used by the recipe (S = "${WORKDIR}/git") contains the whole repository, so paths interpreted relative to it must account for a project whose Cargo.toml lives in a sub directory of the repo: - CARGO_SRC_DIR must point at that sub directory rather than "". - LIC_FILES_CHKSUM entries must be prefixed with it too, otherwise the license paths point at the repo root rather than the actual files. Compute the package directory relative to the git repository working directory in ProjectRepo and use it for both, dropping the previous workspace-root-relative rel_dir helper. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Alex Kiernan --- src/git.rs | 27 +++++++++++++++++++++++++-- src/main.rs | 47 +++++++++++++++-------------------------------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/src/git.rs b/src/git.rs index f637c5d..1f1af01 100644 --- a/src/git.rs +++ b/src/git.rs @@ -16,6 +16,7 @@ use lazy_static::lazy_static; use regex::Regex; use std::default::Default; use std::fmt::{self, Display}; +use std::path::{Path, PathBuf}; /// basic pattern to match ssh style remote URLs /// so that they can be fixed up @@ -90,14 +91,35 @@ pub struct ProjectRepo { pub branch: String, pub rev: String, pub tag: bool, + /// directory containing the package, relative to the root of the git + /// repository (empty when the package lives at the repository root) + pub cargo_src_dir: PathBuf, } impl ProjectRepo { - /// Attempts to guess at the upstream repo this project can be fetched from - pub fn new(gctx: &GlobalContext) -> CargoResult { + /// Attempts to guess at the upstream repo this project can be fetched from. + /// `package_dir` is the directory holding the package's `Cargo.toml`, used + /// to work out where the package sits within the git repository. + pub fn new(gctx: &GlobalContext, package_dir: &Path) -> CargoResult { let repo = Repository::discover(gctx.cwd()) .context("Unable to determine git repo for this project")?; + // the git checkout (S = "${WORKDIR}/git") holds the whole repository, + // so the package may live in a sub directory of it that we need to + // record in CARGO_SRC_DIR + let cargo_src_dir = repo + .workdir() + .map(|workdir| { + let workdir = std::fs::canonicalize(workdir).unwrap_or_else(|_| workdir.to_path_buf()); + let package_dir = + std::fs::canonicalize(package_dir).unwrap_or_else(|_| package_dir.to_path_buf()); + package_dir + .strip_prefix(&workdir) + .map(Path::to_path_buf) + .unwrap_or_default() + }) + .unwrap_or_default(); + let remote = repo .find_remote("origin") .context("Unable to find remote 'origin' for this project")?; @@ -137,6 +159,7 @@ impl ProjectRepo { branch: branch.to_string(), rev: rev.to_string(), tag: Self::rev_is_tag(&repo, &rev), + cargo_src_dir, }) } diff --git a/src/main.rs b/src/main.rs index 17e20de..652f2f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,7 @@ extern crate md5; extern crate regex; extern crate structopt; -use anyhow::{Context as _, anyhow}; +use anyhow::anyhow; use cargo::core::registry::PackageRegistry; use cargo::core::resolver::CliFeatures; use cargo::core::resolver::features::HasDevUnits; @@ -32,7 +32,7 @@ use std::default::Default; use std::env; use std::fs::OpenOptions; use std::io::Write; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use structopt::StructOpt; use structopt::clap::AppSettings; @@ -121,25 +121,6 @@ impl<'gctx> PackageInfo<'gctx> { Ok((packages, resolve)) } - - /// packages that are part of a workspace are a sub directory from the - /// top level which we need to record, this provides us with that - /// relative directory - fn rel_dir(&self) -> CargoResult { - // this is the top level of the workspace - let root = self.ws.root().to_path_buf(); - // path where our current package's Cargo.toml lives - let cwd = self.current_manifest.parent().ok_or_else(|| { - anyhow!( - "Could not get parent of directory '{}'", - self.current_manifest.display() - ) - })?; - - cwd.strip_prefix(&root) - .map(Path::to_path_buf) - .context("Unable to if Cargo.toml is in a sub directory") - } } #[derive(StructOpt, Debug)] @@ -365,29 +346,31 @@ fn real_main(options: Args, gctx: &mut GlobalContext) -> CliResult { String::as_str, ); - // compute the relative directory into the repo our Cargo.toml is at - let rel_dir = md.rel_dir()?; + // attempt to figure out the git repo for this project + let package_dir = md + .current_manifest + .parent() + .expect("Cargo.toml must have a parent"); + let project_repo = git::ProjectRepo::new(gctx, package_dir).unwrap_or_else(|e| { + println!("{}", e); + Default::default() + }); - // license files for the package + // license files for the package; their paths are relative to the git + // checkout root (S = "${WORKDIR}/git"), the same base as CARGO_SRC_DIR let mut lic_files = vec![]; let licenses: Vec<&str> = license.split('/').collect(); let single_license = licenses.len() == 1; for lic in licenses { lic_files.push(format!( " {}", - license::file(crate_root, &rel_dir, lic, single_license) + license::file(crate_root, &project_repo.cargo_src_dir, lic, single_license) )); } // license data in Yocto fmt let license = license.split('/').map(str::trim).join(" | "); - // attempt to figure out the git repo for this project - let project_repo = git::ProjectRepo::new(gctx).unwrap_or_else(|e| { - println!("{}", e); - Default::default() - }); - // if this is not a tag we need to include some data about the version in PV so that // the sstate cache remains valid let git_srcpv = if !project_repo.tag && project_repo.rev.len() > 10 { @@ -432,7 +415,7 @@ fn real_main(options: Args, gctx: &mut GlobalContext) -> CliResult { lic_files = lic_files.join(""), src_uri = src_uris.join(""), src_uri_extras = src_uri_extras.join("\n"), - project_rel_dir = rel_dir.display(), + project_rel_dir = project_repo.cargo_src_dir.display(), project_src_uri = project_repo.uri, project_src_rev = project_repo.rev, git_srcpv = git_srcpv, From 98245868cece306bf03c1f41399f1c0eb1c2a10e Mon Sep 17 00:00:00 2001 From: Alex Kiernan Date: Sun, 31 May 2026 07:01:32 +0100 Subject: [PATCH 3/5] Emit subdir modifier for git deps living in a sub directory A git dependency whose crate is in a sub directory of its repository is fetched to its own destsuffix, but EXTRA_OECARGO_PATHS then points cargo at the repo root rather than the crate. Add a ";subdir=" modifier to the SRC_URI so bitbake unpacks just that sub directory at the destsuffix where cargo expects it. The sub directory is derived from the dependency's checked-out location relative to its git working directory. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Alex Kiernan --- src/git.rs | 80 ++++++++++++++++++++++++++++++++++++++++++----------- src/main.rs | 10 ++++++- 2 files changed, 73 insertions(+), 17 deletions(-) diff --git a/src/git.rs b/src/git.rs index 1f1af01..39f59ba 100644 --- a/src/git.rs +++ b/src/git.rs @@ -53,8 +53,15 @@ impl Display for GitPrefix { } } -/// converts a GIT URL to a Yocto GIT URL -pub fn git_to_yocto_git_url(url: &str, name: Option<&str>, prefix: GitPrefix) -> String { +/// converts a GIT URL to a Yocto GIT URL. +/// `subdir`, when set, tells bitbake to process only that sub directory of the +/// repository (used for git dependencies whose crate lives in a sub directory). +pub fn git_to_yocto_git_url( + url: &str, + name: Option<&str>, + prefix: GitPrefix, + subdir: Option<&str>, +) -> String { // check if its a git@github.com:meta-rust/cargo-bitbake.git style URL // and fix it up if it is let fixed_url = if SSH_STYLE_REMOTE.is_match(url) { @@ -78,10 +85,33 @@ pub fn git_to_yocto_git_url(url: &str, name: Option<&str>, prefix: GitPrefix) -> // by default bitbake only look for SHAs and refs on the master branch. let yocto_url = format!("{};nobranch=1", yocto_url); - if let Some(name) = name { + let yocto_url = if let Some(name) = name { format!("{};name={};destsuffix={}", yocto_url, name, name) } else { yocto_url + }; + + if let Some(subdir) = subdir { + format!("{};subdir={}", yocto_url, subdir) + } else { + yocto_url + } +} + +/// Determine the sub directory within a git checkout that holds the package +/// rooted at `package_dir`. Returns `None` when the package lives at the root +/// of its git repository, so that no `subdir` modifier is emitted. +pub fn git_repo_subdir(package_dir: &Path) -> Option { + let repo = Repository::discover(package_dir).ok()?; + let workdir = repo.workdir()?; + let workdir = std::fs::canonicalize(workdir).unwrap_or_else(|_| workdir.to_path_buf()); + let package_dir = + std::fs::canonicalize(package_dir).unwrap_or_else(|_| package_dir.to_path_buf()); + let subdir = package_dir.strip_prefix(&workdir).ok()?; + if subdir.as_os_str().is_empty() { + None + } else { + Some(subdir.to_string_lossy().into_owned()) } } @@ -136,7 +166,7 @@ impl ProjectRepo { let uri = remote .url() .ok_or_else(|| anyhow!("No URL for remote 'origin'"))?; - let uri = git_to_yocto_git_url(uri, None, prefix); + let uri = git_to_yocto_git_url(uri, None, prefix, None); let head = repo.head().context("Unable to find HEAD")?; let branch = head @@ -188,7 +218,7 @@ mod test { #[test] fn remote_http() { let repo = "http://github.com/rust-lang/cargo.git"; - let url = git_to_yocto_git_url(repo, Some("cargo"), GitPrefix::Git); + let url = git_to_yocto_git_url(repo, Some("cargo"), GitPrefix::Git, None); assert_eq!(url, "git://github.com/rust-lang/cargo.git;protocol=http;nobranch=1;name=cargo;destsuffix=cargo"); } @@ -196,7 +226,7 @@ mod test { #[test] fn remote_https() { let repo = "https://github.com/rust-lang/cargo.git"; - let url = git_to_yocto_git_url(repo, Some("cargo"), GitPrefix::Git); + let url = git_to_yocto_git_url(repo, Some("cargo"), GitPrefix::Git, None); assert_eq!(url, "git://github.com/rust-lang/cargo.git;protocol=https;nobranch=1;name=cargo;destsuffix=cargo"); } @@ -204,7 +234,7 @@ mod test { #[test] fn remote_ssh() { let repo = "git@github.com:rust-lang/cargo.git"; - let url = git_to_yocto_git_url(repo, Some("cargo"), GitPrefix::Git); + let url = git_to_yocto_git_url(repo, Some("cargo"), GitPrefix::Git, None); assert_eq!(url, "git://git@github.com/rust-lang/cargo.git;protocol=ssh;nobranch=1;name=cargo;destsuffix=cargo"); } @@ -212,7 +242,7 @@ mod test { #[test] fn remote_http_nosuffix() { let repo = "http://github.com/rust-lang/cargo.git"; - let url = git_to_yocto_git_url(repo, None, GitPrefix::Git); + let url = git_to_yocto_git_url(repo, None, GitPrefix::Git, None); assert_eq!( url, "git://github.com/rust-lang/cargo.git;protocol=http;nobranch=1" @@ -222,7 +252,7 @@ mod test { #[test] fn remote_https_nosuffix() { let repo = "https://github.com/rust-lang/cargo.git"; - let url = git_to_yocto_git_url(repo, None, GitPrefix::Git); + let url = git_to_yocto_git_url(repo, None, GitPrefix::Git, None); assert_eq!( url, "git://github.com/rust-lang/cargo.git;protocol=https;nobranch=1" @@ -232,7 +262,7 @@ mod test { #[test] fn remote_ssh_nosuffix() { let repo = "git@github.com:rust-lang/cargo.git"; - let url = git_to_yocto_git_url(repo, None, GitPrefix::Git); + let url = git_to_yocto_git_url(repo, None, GitPrefix::Git, None); assert_eq!( url, "git://git@github.com/rust-lang/cargo.git;protocol=ssh;nobranch=1" @@ -242,7 +272,7 @@ mod test { #[test] fn cargo_http() { let repo = "http://github.com/rust-lang/cargo.git"; - let url = git_to_yocto_git_url(repo, Some("cargo"), GitPrefix::Git); + let url = git_to_yocto_git_url(repo, Some("cargo"), GitPrefix::Git, None); assert_eq!(url, "git://github.com/rust-lang/cargo.git;protocol=http;nobranch=1;name=cargo;destsuffix=cargo"); } @@ -250,7 +280,7 @@ mod test { #[test] fn cargo_https() { let repo = "https://github.com/rust-lang/cargo.git"; - let url = git_to_yocto_git_url(repo, Some("cargo"), GitPrefix::Git); + let url = git_to_yocto_git_url(repo, Some("cargo"), GitPrefix::Git, None); assert_eq!(url, "git://github.com/rust-lang/cargo.git;protocol=https;nobranch=1;name=cargo;destsuffix=cargo"); } @@ -258,7 +288,7 @@ mod test { #[test] fn cargo_ssh() { let repo = "ssh://git@github.com/rust-lang/cargo.git"; - let url = git_to_yocto_git_url(repo, Some("cargo"), GitPrefix::Git); + let url = git_to_yocto_git_url(repo, Some("cargo"), GitPrefix::Git, None); assert_eq!(url, "git://git@github.com/rust-lang/cargo.git;protocol=ssh;nobranch=1;name=cargo;destsuffix=cargo"); } @@ -266,7 +296,7 @@ mod test { #[test] fn cargo_ssh_with_port() { let repo = "ssh://git@git.example.com:222/foo/bar.git"; - let url = git_to_yocto_git_url(repo, Some("bar"), GitPrefix::Git); + let url = git_to_yocto_git_url(repo, Some("bar"), GitPrefix::Git, None); assert_eq!(url, "git://git@git.example.com:222/foo/bar.git;protocol=ssh;nobranch=1;name=bar;destsuffix=bar"); } @@ -274,7 +304,7 @@ mod test { #[test] fn cargo_ssh_with_port_nosuffix() { let repo = "ssh://git@git.example.com:222/foo/bar.git"; - let url = git_to_yocto_git_url(repo, None, GitPrefix::Git); + let url = git_to_yocto_git_url(repo, None, GitPrefix::Git, None); assert_eq!( url, "git://git@git.example.com:222/foo/bar.git;protocol=ssh;nobranch=1" @@ -284,8 +314,26 @@ mod test { #[test] fn remote_ssh_with_submodules() { let repo = "git@github.com:rust-lang/cargo.git"; - let url = git_to_yocto_git_url(repo, Some("cargo"), GitPrefix::GitSubmodule); + let url = git_to_yocto_git_url(repo, Some("cargo"), GitPrefix::GitSubmodule, None); assert_eq!(url, "gitsm://git@github.com/rust-lang/cargo.git;protocol=ssh;nobranch=1;name=cargo;destsuffix=cargo"); } + + #[test] + fn cargo_https_with_subdir() { + let repo = "https://github.com/rust-lang/cargo.git"; + let url = git_to_yocto_git_url(repo, Some("cargo"), GitPrefix::Git, Some("crates/cargo")); + assert_eq!(url, + "git://github.com/rust-lang/cargo.git;protocol=https;nobranch=1;name=cargo;destsuffix=cargo;subdir=crates/cargo"); + } + + #[test] + fn cargo_https_nosuffix_with_subdir() { + let repo = "https://github.com/rust-lang/cargo.git"; + let url = git_to_yocto_git_url(repo, None, GitPrefix::Git, Some("crates/cargo")); + assert_eq!( + url, + "git://github.com/rust-lang/cargo.git;protocol=https;nobranch=1;subdir=crates/cargo" + ); + } } diff --git a/src/main.rs b/src/main.rs index 652f2f7..ae72746 100644 --- a/src/main.rs +++ b/src/main.rs @@ -205,7 +205,7 @@ fn real_main(options: Args, gctx: &mut GlobalContext) -> CliResult { } // Resolve all dependencies (generate or use Cargo.lock as necessary) - let resolve = md.resolve()?.1; + let (package_set, resolve) = md.resolve()?; let pkg_checksums = resolve.checksums(); // build the crate URIs @@ -242,10 +242,18 @@ fn real_main(options: Args, gctx: &mut GlobalContext) -> CliResult { // Just use the default download method for git repositories // found in the source URIs, since cargo currently cannot // initialize submodules for git dependencies anyway. + // + // If the crate lives in a sub directory of its git repo we + // emit a subdir modifier so bitbake processes only that subdir. + let subdir = package_set + .get_one(pkg) + .ok() + .and_then(|p| git::git_repo_subdir(p.root())); let url = git::git_to_yocto_git_url( src_id.url().as_str(), Some(pkg.name().as_str()), git::GitPrefix::default(), + subdir.as_deref(), ); // save revision From 356a5452ac6aaca97b82cd1859f9b2154d769c43 Mon Sep 17 00:00:00 2001 From: Alex Kiernan Date: Sun, 31 May 2026 07:35:26 +0100 Subject: [PATCH 4/5] Fall back to the git repo root when locating license files License-file discovery only searched the crate directory, so a shared license at the top of a monorepo (with the crate in a sub directory) was never found and emitted the generateme fallback. Search the crate directory first, then the git repository root, emitting paths relative to S = "${WORKDIR}/git" in both cases (the crate sub directory prefix for the former, no prefix for the latter). Track the repo root in ProjectRepo to support this. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Alex Kiernan --- src/git.rs | 26 ++++++++++------- src/license.rs | 76 ++++++++++++++++++++++++++------------------------ src/main.rs | 8 +++++- 3 files changed, 62 insertions(+), 48 deletions(-) diff --git a/src/git.rs b/src/git.rs index 39f59ba..f82ff9d 100644 --- a/src/git.rs +++ b/src/git.rs @@ -124,6 +124,9 @@ pub struct ProjectRepo { /// directory containing the package, relative to the root of the git /// repository (empty when the package lives at the repository root) pub cargo_src_dir: PathBuf, + /// the root of the git repository's working directory (empty when no git + /// repository could be found) + pub repo_dir: PathBuf, } impl ProjectRepo { @@ -137,18 +140,20 @@ impl ProjectRepo { // the git checkout (S = "${WORKDIR}/git") holds the whole repository, // so the package may live in a sub directory of it that we need to // record in CARGO_SRC_DIR - let cargo_src_dir = repo + let repo_dir = repo .workdir() - .map(|workdir| { - let workdir = std::fs::canonicalize(workdir).unwrap_or_else(|_| workdir.to_path_buf()); - let package_dir = - std::fs::canonicalize(package_dir).unwrap_or_else(|_| package_dir.to_path_buf()); - package_dir - .strip_prefix(&workdir) - .map(Path::to_path_buf) - .unwrap_or_default() - }) + .map(|workdir| std::fs::canonicalize(workdir).unwrap_or_else(|_| workdir.to_path_buf())) .unwrap_or_default(); + let cargo_src_dir = if repo_dir.as_os_str().is_empty() { + PathBuf::new() + } else { + let package_dir = + std::fs::canonicalize(package_dir).unwrap_or_else(|_| package_dir.to_path_buf()); + package_dir + .strip_prefix(&repo_dir) + .map(Path::to_path_buf) + .unwrap_or_default() + }; let remote = repo .find_remote("origin") @@ -190,6 +195,7 @@ impl ProjectRepo { rev: rev.to_string(), tag: Self::rev_is_tag(&repo, &rev), cargo_src_dir, + repo_dir, }) } diff --git a/src/license.rs b/src/license.rs index 438c91e..70bfb73 100644 --- a/src/license.rs +++ b/src/license.rs @@ -24,9 +24,18 @@ fn file_md5>(license_file: P) -> Result { Ok(format!("{:x}", context.compute())) } -/// Given the top level of the crate at `crate_root`, attempt to find -/// the license file based on the name of the license in `license_name`. -pub fn file(crate_root: &Path, rel_dir: &Path, license_name: &str, single_license: bool) -> String { +/// Attempt to find the license file based on the name of the license in +/// `license_name`. The crate directory (`crate_root`, emitted relative to the +/// git checkout as `rel_dir`) is searched first; failing that the git +/// repository root (`repo_root`) is searched, which catches a shared license +/// at the top of a monorepo. Paths are emitted relative to S = "${WORKDIR}/git". +pub fn file( + crate_root: &Path, + rel_dir: &Path, + repo_root: &Path, + license_name: &str, + single_license: bool, +) -> String { // CLOSED is a special case license (case sensitive) per // http://www.yoctoproject.org/docs/2.3.2/mega-manual/mega-manual.html#sdk-license-detection // that means this is closed source and there is no license @@ -35,42 +44,35 @@ pub fn file(crate_root: &Path, rel_dir: &Path, license_name: &str, single_licens return "".into(); } - // if the license exists at the top level then - // return the right URL to it. try to handle the special - // case license path we support as well + // candidate file names to look for, in priority order. We also handle the + // special case license path we support as well as a plain LICENSE file. let special_name = format!("LICENSE-{}", license_name); - let lic_path = Path::new(license_name); - let spec_path = Path::new(&special_name); - let simple_path = Path::new("LICENSE"); + let mut candidates: Vec<&Path> = vec![Path::new(license_name), Path::new(&special_name)]; + if single_license { + candidates.push(Path::new("LICENSE")); + } - let lic_abs_path = crate_root.join(lic_path); - let spec_abs_path = crate_root.join(spec_path); - let simple_abs_path = crate_root.join(simple_path); + // where to look, with the prefix to emit for files found there. The crate + // directory wins over the repo root so a crate-local license takes priority. + let mut search: Vec<(&Path, &Path)> = vec![(crate_root, rel_dir)]; + if !repo_root.as_os_str().is_empty() && repo_root != crate_root { + search.push((repo_root, Path::new(""))); + } - if lic_abs_path.exists() { - let md5sum = file_md5(lic_abs_path).unwrap_or_else(|_| String::from("generateme")); - format!( - "file://{};md5={} \\\n", - rel_dir.join(lic_path).display(), - md5sum - ) - } else if spec_abs_path.exists() { - // the special case - let md5sum = file_md5(spec_abs_path).unwrap_or_else(|_| String::from("generateme")); - format!( - "file://{};md5={} \\\n", - rel_dir.join(spec_path).display(), - md5sum - ) - } else if simple_abs_path.exists() && single_license { - let md5sum = file_md5(simple_abs_path).unwrap_or_else(|_| String::from("generateme")); - format!( - "file://{};md5={} \\\n", - rel_dir.join(simple_path).display(), - md5sum - ) - } else { - // fall through - format!("file://{};md5=generateme \\\n", license_name) + for (dir, prefix) in search { + for cand in &candidates { + let abs_path = dir.join(cand); + if abs_path.exists() { + let md5sum = file_md5(&abs_path).unwrap_or_else(|_| String::from("generateme")); + return format!( + "file://{};md5={} \\\n", + prefix.join(cand).display(), + md5sum + ); + } + } } + + // fall through + format!("file://{};md5=generateme \\\n", license_name) } diff --git a/src/main.rs b/src/main.rs index ae72746..aaa7127 100644 --- a/src/main.rs +++ b/src/main.rs @@ -372,7 +372,13 @@ fn real_main(options: Args, gctx: &mut GlobalContext) -> CliResult { for lic in licenses { lic_files.push(format!( " {}", - license::file(crate_root, &project_repo.cargo_src_dir, lic, single_license) + license::file( + crate_root, + &project_repo.cargo_src_dir, + &project_repo.repo_dir, + lic, + single_license, + ) )); } From 18d9f95be283cb256462b688bf361936ff323712 Mon Sep 17 00:00:00 2001 From: Alex Kiernan Date: Sun, 31 May 2026 07:45:13 +0100 Subject: [PATCH 5/5] Locate license files relative to the selected package crate_root was the workspace root, so a workspace member's own license file (in the member directory) was never found and fell back to generateme. Use the selected package's directory as crate_root and its git-root-relative path as the license prefix, so member-local licenses are discovered and pathed correctly. The repo-root fallback still handles a shared license at the top of the workspace. CARGO_SRC_DIR is unchanged (the workspace root, built with -p). Co-Authored-By: Claude Opus 4.8 Signed-off-by: Alex Kiernan --- src/main.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index aaa7127..8242710 100644 --- a/src/main.rs +++ b/src/main.rs @@ -194,11 +194,9 @@ fn real_main(options: Args, gctx: &mut GlobalContext) -> CliResult { // Our current package let package = md.package()?; - let crate_root = md - .ws - .root_manifest() - .parent() - .expect("Cargo.toml must have a parent"); + // the directory of the selected package, where its license files live (for + // a workspace member this is the member directory, not the workspace root) + let crate_root = package.root(); if package.name().contains('_') { println!("Package name contains an underscore"); @@ -365,7 +363,12 @@ fn real_main(options: Args, gctx: &mut GlobalContext) -> CliResult { }); // license files for the package; their paths are relative to the git - // checkout root (S = "${WORKDIR}/git"), the same base as CARGO_SRC_DIR + // checkout root (S = "${WORKDIR}/git"). The selected package's own + // directory within the repo is the prefix for a crate-local license (this + // is the member directory for a workspace member, not the workspace root). + let crate_rel_dir = git::git_repo_subdir(crate_root) + .map(PathBuf::from) + .unwrap_or_default(); let mut lic_files = vec![]; let licenses: Vec<&str> = license.split('/').collect(); let single_license = licenses.len() == 1; @@ -374,7 +377,7 @@ fn real_main(options: Args, gctx: &mut GlobalContext) -> CliResult { " {}", license::file( crate_root, - &project_repo.cargo_src_dir, + &crate_rel_dir, &project_repo.repo_dir, lic, single_license,