diff --git a/src/git.rs b/src/git.rs index f637c5d..f82ff9d 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 @@ -52,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) { @@ -77,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()) } } @@ -90,14 +121,40 @@ 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, + /// the root of the git repository's working directory (empty when no git + /// repository could be found) + pub repo_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 repo_dir = repo + .workdir() + .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") .context("Unable to find remote 'origin' for this project")?; @@ -114,7 +171,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 @@ -137,6 +194,8 @@ impl ProjectRepo { branch: branch.to_string(), rev: rev.to_string(), tag: Self::rev_is_tag(&repo, &rev), + cargo_src_dir, + repo_dir, }) } @@ -165,7 +224,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"); } @@ -173,7 +232,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"); } @@ -181,7 +240,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"); } @@ -189,7 +248,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" @@ -199,7 +258,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" @@ -209,7 +268,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" @@ -219,7 +278,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"); } @@ -227,7 +286,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"); } @@ -235,7 +294,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"); } @@ -243,7 +302,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"); } @@ -251,7 +310,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" @@ -261,8 +320,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/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 07a96c7..8242710 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)] @@ -213,18 +194,16 @@ 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"); } // 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 @@ -261,10 +240,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 @@ -334,20 +321,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 @@ -366,29 +352,42 @@ 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 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; for lic in licenses { lic_files.push(format!( " {}", - license::file(crate_root, &rel_dir, lic, single_license) + license::file( + crate_root, + &crate_rel_dir, + &project_repo.repo_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 { @@ -433,7 +432,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,