diff --git a/Cargo.lock b/Cargo.lock index fd7fcf82..7e298619 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -218,6 +218,10 @@ name = "cc" version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" +dependencies = [ + "jobserver", + "libc", +] [[package]] name = "cfg-if" @@ -341,6 +345,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "copy_dir" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "543d1dd138ef086e2ff05e3a48cf9da045da2033d16f8538fd76b86cd49b2ca3" +dependencies = [ + "walkdir", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -749,6 +762,21 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "git2" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" +dependencies = [ + "bitflags 2.6.0", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + [[package]] name = "h2" version = "0.3.26" @@ -1054,6 +1082,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.69" @@ -1095,6 +1132,46 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libgit2-sys" +version = "0.17.0+1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libssh2-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "line-index" version = "0.1.1" @@ -1264,10 +1341,12 @@ dependencies = [ "bytes", "clap", "colored", + "copy_dir", "dialoguer", "dunce", "expect-test", "form_urlencoded", + "git2", "indexmap", "log", "moonutil", @@ -1278,9 +1357,11 @@ dependencies = [ "serde_json", "serde_json_lenient", "sha2", + "tempfile", "test-log", "thiserror", "tokio", + "url", "walkdir", "zip", ] @@ -1308,6 +1389,7 @@ dependencies = [ "serde", "serde_json_lenient", "thiserror", + "twox-hash", "vergen", "walkdir", ] @@ -1968,6 +2050,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -2265,6 +2353,17 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "rand", + "static_assertions", +] + [[package]] name = "typenum" version = "1.17.0" diff --git a/Cargo.toml b/Cargo.toml index cf3a444c..808508b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,7 @@ test-log = "0.2" fs4 = { version = "0.8.3", features = ["sync"] } ariadne = { version = "0.4.1", features = ["auto-color"] } clap_complete = { version = "4.5.4" } +twox-hash = { version = "1.6.3" } [profile.release] debug = false diff --git a/crates/mooncake/Cargo.toml b/crates/mooncake/Cargo.toml index c4cbf1f4..7083b3fc 100644 --- a/crates/mooncake/Cargo.toml +++ b/crates/mooncake/Cargo.toml @@ -46,7 +46,11 @@ indexmap.workspace = true petgraph.workspace = true thiserror.workspace = true dunce.workspace = true +git2 = "0.19.0" +url = "2.5.2" [dev-dependencies] expect-test.workspace = true test-log.workspace = true +tempfile.workspace = true +copy_dir = "0.1.3" diff --git a/crates/mooncake/src/dep_dir.rs b/crates/mooncake/src/dep_dir.rs index 4fe09537..5d11babb 100644 --- a/crates/mooncake/src/dep_dir.rs +++ b/crates/mooncake/src/dep_dir.rs @@ -265,9 +265,7 @@ fn map_source_to_dir(dep_dir: &DepDir, module: &ModuleSource) -> PathBuf { pkg_to_dir(dep_dir, &module.name.username, &module.name.pkgname) } ModuleSourceKind::Local(path) => path.clone(), - ModuleSourceKind::Git(url) => { - todo!("Git dependency is not yet supported. Got git url: {}", url) - } + ModuleSourceKind::Git(url) => crate::resolver::git::resolve(url).unwrap(), } } diff --git a/crates/mooncake/src/resolver.rs b/crates/mooncake/src/resolver.rs index 875923f1..35712ac0 100644 --- a/crates/mooncake/src/resolver.rs +++ b/crates/mooncake/src/resolver.rs @@ -26,6 +26,7 @@ use thiserror::Error; use crate::registry::RegistryList; pub mod env; +pub mod git; pub mod mvs; pub use mvs::MvsSolver; diff --git a/crates/mooncake/src/resolver/env.rs b/crates/mooncake/src/resolver/env.rs index 9cd953ae..ffc39718 100644 --- a/crates/mooncake/src/resolver/env.rs +++ b/crates/mooncake/src/resolver/env.rs @@ -22,21 +22,23 @@ use std::{ rc::Rc, }; +use anyhow::Context; use moonutil::{ common::read_module_desc_file_in_dir, module::MoonMod, - mooncakes::{ModuleName, ModuleSource, ModuleSourceKind}, + mooncakes::{GitSource, ModuleName, ModuleSource, ModuleSourceKind}, }; use semver::Version; use crate::registry::RegistryList; -use super::ResolverError; +use super::{git::recursively_scan_for_moon_mods, ResolverError}; pub struct ResolverEnv<'a> { registries: &'a RegistryList, errors: Vec, local_module_cache: HashMap>, + git_module_cache: HashMap)>>, } impl<'a> ResolverEnv<'a> { @@ -45,6 +47,7 @@ impl<'a> ResolverEnv<'a> { registries, errors: Vec::new(), local_module_cache: HashMap::new(), + git_module_cache: HashMap::new(), } } @@ -104,4 +107,44 @@ impl<'a> ResolverEnv<'a> { .insert(path.to_owned(), Rc::clone(&rc_module)); Ok(rc_module) } + + pub fn resolve_git_module( + &mut self, + git_info: &GitSource, + expected_name: &ModuleName, + ) -> Result, ResolverError> { + // Check cache + if let Some(mods) = self.git_module_cache.get(git_info) { + if let Some((_, module)) = mods.get(expected_name) { + return Ok(module.clone()); + } + } + + let checkout = super::git::resolve(git_info) + .with_context(|| format!("Failed to resolve git source {}", git_info)) + .map_err(ResolverError::Other)?; + let mods = recursively_scan_for_moon_mods(&checkout) + .with_context(|| format!("Failed to scan for moon mods in {}", checkout.display())) + .map_err(ResolverError::Other)?; + + // populate cache + let mut mods_map = HashMap::new(); + for (path, module) in mods { + mods_map.insert( + module.name.parse().map_err(|e| { + ResolverError::Other(anyhow::anyhow!("Failed to parse module name: {}", e)) + })?, + (path, module.clone()), + ); + } + let entry = self + .git_module_cache + .entry(git_info.clone()) + .or_insert(mods_map); + + entry + .get(expected_name) + .map(|(_, module)| module.clone()) + .ok_or_else(|| ResolverError::ModuleMissing(expected_name.clone())) + } } diff --git a/crates/mooncake/src/resolver/git.rs b/crates/mooncake/src/resolver/git.rs new file mode 100644 index 00000000..b96d0a8a --- /dev/null +++ b/crates/mooncake/src/resolver/git.rs @@ -0,0 +1,379 @@ +// moon: The build system and package manager for MoonBit. +// Copyright (C) 2024 International Digital Economy Academy +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// For inquiries, you can contact us via e-mail at jichuruanjian@idea.edu.cn. + +//! Resolves git-sourced modules. +//! +//! Git modules reside in 2 different locations: +//! - A raw git repository is checked out into the registry cache. +//! - For each different revision of the git repository, a separate directory is created in the +//! registry cache, and is checked out into that directory. +//! +//! This mimics the behavior of cargo's git dependencies. + +use std::{ + path::{Path, PathBuf}, + rc::Rc, +}; + +use anyhow::Context; +use git2::{build::CheckoutBuilder, FetchOptions, Oid, Repository}; +use moonutil::{ + common::{read_module_desc_file_in_dir, read_module_from_json, MOON_MOD_JSON}, + hash::short_hash, + module::MoonMod, + moon_dir::{git_checkouts_dir, git_repos_dir}, + mooncakes::GitSource, +}; +use url::Url; +use walkdir::WalkDir; + +fn ident(url: &Url) -> &str { + url.path_segments() + .and_then(|s| s.last()) + .unwrap_or("") + .trim_end_matches(".git") +} + +fn repo_name(url: &Url) -> String { + let id = ident(url); + let hash = short_hash(url); + format!("{}-{}", id, hash) +} + +fn repo_path(url: &Url) -> PathBuf { + git_repos_dir().join(repo_name(url)) +} + +fn repo_checkout_path(url: &Url, commit: Oid) -> PathBuf { + git_checkouts_dir() + .join(repo_name(url)) + .join(commit.to_string()) +} + +pub fn init_repo_dir(url: &Url) -> anyhow::Result<(PathBuf, Repository)> { + let path = repo_path(url); + std::fs::create_dir_all(&path).context("failed to create git repository directory")?; + git2::Repository::init_bare(&path).context("failed to initialize git repository")?; + let repo = git2::Repository::open_bare(&path).context("failed to open git repository")?; + repo.remote("origin", url.as_str())?; + + Ok((path, repo)) +} + +pub fn open_or_init_repo_dir(url: &Url) -> anyhow::Result<(PathBuf, Repository)> { + let path = repo_path(url); + if path.exists() { + let repo = git2::Repository::open_bare(&path).context("failed to open git repository")?; + Ok((path, repo)) + } else { + init_repo_dir(url) + } +} + +fn pull_branch(repo: &Repository, branch: &str) -> anyhow::Result { + let mut remote = repo.find_remote("origin")?; + if !remote.connected() { + remote.connect(git2::Direction::Fetch)?; + } + + remote + .fetch(&[branch], None, None) + .context("Failed to fetch from remote")?; + + let branch_ref = format!("refs/remotes/origin/{}", branch); + let branch_ref = repo.find_reference(&branch_ref)?; + let branch_commit = branch_ref.peel_to_commit()?; + Ok(branch_commit.id()) +} + +fn pull_default_branch(repo: &Repository) -> anyhow::Result { + let mut remote = repo.find_remote("origin")?; + // if the default branch is not set, we connect to the remote and try again + let default_branch = if let Ok(default_branch) = remote.default_branch() { + default_branch + } else { + remote.connect(git2::Direction::Fetch)?; + remote + .default_branch() + .context("Failed to get default branch")? + }; + let default_branch = default_branch + .as_str() + .ok_or_else(|| anyhow::anyhow!("Malformed default branch name in remote"))?; + let default_branch = default_branch.trim_start_matches("refs/heads/"); + let res = pull_branch(repo, default_branch); + remote.disconnect()?; + res +} + +fn pull_specific_revision(repo: &Repository, revision: &str) -> anyhow::Result { + let mut remote = repo.find_remote("origin")?; + remote.fetch(&[revision], Some(FetchOptions::new().depth(1)), None)?; + let commit = repo.revparse_single(revision)?; + Ok(commit.id()) +} + +fn checkout(repo: &Repository, commit: Oid, dst: &Path) -> anyhow::Result<()> { + let commit = repo.find_commit(commit)?; + let tree = commit.tree()?; + repo.checkout_tree( + &tree.into_object(), + Some(CheckoutBuilder::new().target_dir(dst)), + )?; + Ok(()) +} + +pub fn resolve(source: &GitSource) -> anyhow::Result { + let source_url = Url::parse(&source.url).context("Malformed git source url")?; + // Open or initialize the repository. + let (_, repo) = open_or_init_repo_dir(&source_url)?; + // Find the revision to checkout. + let commit = if let Some(branch) = &source.branch { + pull_branch(&repo, branch)? + } else if let Some(revision) = &source.revision { + pull_specific_revision(&repo, revision)? + } else { + pull_default_branch(&repo)? + }; + // Checkout the revision to the cache. + let checkout_path = repo_checkout_path(&source_url, commit); + if !checkout_path.exists() { + std::fs::create_dir_all(&checkout_path)?; + checkout(&repo, commit, &checkout_path)?; + } + Ok(checkout_path) +} + +pub fn recursively_scan_for_moon_mods(path: &Path) -> anyhow::Result)>> { + let mut mods = Vec::new(); + for entry in WalkDir::new(path) { + let entry = entry?; + if entry.file_name() == MOON_MOD_JSON { + let dir = entry.path().parent().unwrap(); + let module = read_module_from_json(entry.path()).context("Failed to read module")?; + mods.push((dir.into(), Rc::new(module))); + } + } + Ok(mods) +} + +#[cfg(test)] +mod test { + use std::path::Path; + + use expect_test::expect; + use walkdir::WalkDir; + + const SAMPLE_GIT_REPO_DIR: &str = "tests/git_test_template"; + + fn make_sample_git_repo(path: &Path) { + // cp -r crates/mooncake/test/git_test_template/* repo/ + copy_dir::copy_dir(SAMPLE_GIT_REPO_DIR, path).unwrap(); + + // git init . + let repo = git2::Repository::init(path).unwrap(); + + // config user.{name,email} + repo.config() + .unwrap() + .set_str("user.name", "mooncake-tester") + .unwrap(); + repo.config() + .unwrap() + .set_str("user.email", "me@example.com") + .unwrap(); + + // git add . + let mut index = repo.index().unwrap(); + index + .add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None) + .unwrap(); + index.write().unwrap(); + let tree_id = index.write_tree().unwrap(); + let tree = repo.find_tree(tree_id).unwrap(); + + // git commit -m "Initial commit" + let head = repo + .commit( + Some("HEAD"), + &repo.signature().unwrap(), + &repo.signature().unwrap(), + "Initial commit", + &tree, + &[], + ) + .unwrap(); + + // git branch main + let commit = repo.find_commit(head).unwrap(); + repo.branch("main", &commit, false).unwrap(); + repo.set_head("refs/heads/main").unwrap(); + + // generate another branch with a different file + + // git checkout -b other + repo.branch("other", &commit, false).unwrap(); + repo.set_head("refs/heads/other").unwrap(); + + // echo "other file" > other_file + let other_path = path.join("other_file"); + std::fs::write(&other_path, "other file").unwrap(); + + // git add other_file + index.add_path(Path::new("other_file")).unwrap(); + index.write().unwrap(); + + // git commit -m "Add other file" + let tree_id = index.write_tree().unwrap(); + let tree = repo.find_tree(tree_id).unwrap(); + repo.commit( + Some("HEAD"), + &repo.signature().unwrap(), + &repo.signature().unwrap(), + "Add other file", + &tree, + &[&commit], + ) + .unwrap(); + + // set default branch back to main + repo.set_head("refs/heads/main").unwrap(); + } + + fn list_dir_contents(dir: &Path) -> String { + WalkDir::new(dir) + .sort_by_file_name() + .into_iter() + .map(|e| { + e.unwrap() + .path() + .strip_prefix(dir) + .unwrap() + .to_string_lossy() + .to_string() + }) + .collect::>() + .join("\n") + } + + #[test] + fn test_basic_git_resolver() { + let temp_dir = tempfile::tempdir().unwrap(); + let repo_path = temp_dir.path().join("repo"); + make_sample_git_repo(&repo_path); + + let test_moon_home = temp_dir.path().join("moon_home"); + std::fs::create_dir_all(&test_moon_home).unwrap(); + std::env::set_var("MOON_HOME", test_moon_home); + + // Try to resolve the git repository. + let source = moonutil::mooncakes::GitSource { + url: format!("file://{}", repo_path.display()), + branch: None, + revision: None, + }; + + let checkout = super::resolve(&source).unwrap(); + assert!(checkout.exists()); + + // Check that the checkout is correct. + let dir_contents = list_dir_contents(&checkout); + expect![[r#" + + README.md + lib + lib/hello.mbt + lib/moon.pkg.json + moon.mod.json + nonroot_pkg + nonroot_pkg/README.md + nonroot_pkg/lib + nonroot_pkg/lib/hello.mbt + nonroot_pkg/lib/moon.pkg.json + nonroot_pkg/moon.mod.json"#]] + .assert_eq(&dir_contents); + } + + #[test] + fn test_git_with_branch_specified() { + let temp_dir = tempfile::tempdir().unwrap(); + let repo_path = temp_dir.path().join("repo"); + make_sample_git_repo(&repo_path); + + let test_moon_home = temp_dir.path().join("moon_home"); + std::fs::create_dir_all(&test_moon_home).unwrap(); + std::env::set_var("MOON_HOME", test_moon_home); + + // Try to resolve the git repository. + let source = moonutil::mooncakes::GitSource { + url: format!("file://{}", repo_path.display()), + branch: Some("other".into()), + revision: None, + }; + + let checkout = super::resolve(&source).unwrap(); + assert!(checkout.exists()); + + // Check that the checkout is correct. + let dir_contents = list_dir_contents(&checkout); + expect![[r#" + + README.md + lib + lib/hello.mbt + lib/moon.pkg.json + moon.mod.json + nonroot_pkg + nonroot_pkg/README.md + nonroot_pkg/lib + nonroot_pkg/lib/hello.mbt + nonroot_pkg/lib/moon.pkg.json + nonroot_pkg/moon.mod.json + other_file"#]] + .assert_eq(&dir_contents); + } + + #[test] + fn test_find_all_packages() { + let temp_dir = tempfile::tempdir().unwrap(); + let repo_path = temp_dir.path().join("repo"); + make_sample_git_repo(&repo_path); + + let test_moon_home = temp_dir.path().join("moon_home"); + std::fs::create_dir_all(&test_moon_home).unwrap(); + std::env::set_var("MOON_HOME", test_moon_home); + + // Try to resolve the git repository. + let source = moonutil::mooncakes::GitSource { + url: format!("file://{}", repo_path.display()), + branch: None, + revision: None, + }; + + let checkout = super::resolve(&source).unwrap(); + assert!(checkout.exists()); + + // Check that the checkout is correct. + let mods = super::recursively_scan_for_moon_mods(&checkout).unwrap(); + let mods = mods + .into_iter() + .map(|(_, module)| module.name.clone()) + .collect::>(); + assert_eq!(mods, vec!["testing/test", "testing/nonroot-test"]); + } +} diff --git a/crates/mooncake/src/resolver/mvs.rs b/crates/mooncake/src/resolver/mvs.rs index 0d754f76..eb5ffd73 100644 --- a/crates/mooncake/src/resolver/mvs.rs +++ b/crates/mooncake/src/resolver/mvs.rs @@ -26,7 +26,7 @@ use anyhow::anyhow; use moonutil::{ dependency::DependencyInfo, module::MoonMod, - mooncakes::{ModuleName, ModuleSource, ModuleSourceKind}, + mooncakes::{GitSource, ModuleName, ModuleSource, ModuleSourceKind}, version::as_caret_comparator, }; use semver::Version; @@ -342,39 +342,11 @@ fn resolve_pkg( ) -> Result<(ModuleSource, Rc), ResolverError> { if let Some(path) = &req.path { if local_dep_allowed(dependant) { - // Try resolving using local dependency - let root = root_path_of(dependant); - assert!( - root.is_absolute(), - "Root path of {} is not absolute! Got: {}", - dependant, - root.display() - ); - let dep_path = root.join(path); - let dep_path = - dunce::canonicalize(dep_path).map_err(|err| ResolverError::Other(err.into()))?; - let res = env.resolve_local_module(&dep_path)?; - let ms = ModuleSource { - name: pkg_name.clone(), - version: res.version.clone().expect("Expected version in module"), - source: ModuleSourceKind::Local(dep_path), - }; - // Assert version matches - if let Some(v) = &res.version { - if !req.version.matches(v) { - return Err(ResolverError::LocalDepVersionMismatch( - Box::new(ms), - req.version.clone(), - )); - } - } - return Ok((ms, res)); + return resolve_pkg_local(dependant, path, env, pkg_name, req); } } - if let Some(_url) = &req.git { - if git_dep_allowed(dependant) { - // TODO: Try resolving using git dependency - } + if req.git.is_some() && git_dep_allowed(dependant) { + return resolve_pkg_git(req, env, pkg_name); } // If neither git nor local dependencies can be resolved (either because the user // didn't specify it at all, or because the repo comes from a registry), we fallback @@ -394,6 +366,61 @@ fn resolve_pkg( Ok((ms, module)) } +fn resolve_pkg_local( + dependant: &ModuleSource, + path: &String, + env: &mut ResolverEnv, + pkg_name: &ModuleName, + req: &DependencyInfo, +) -> Result<(ModuleSource, Rc), ResolverError> { + // Try resolving using local dependency + let root = root_path_of(dependant); + assert!( + root.is_absolute(), + "Root path of {} is not absolute! Got: {}", + dependant, + root.display() + ); + let dep_path = root.join(path); + let dep_path = dunce::canonicalize(dep_path).map_err(|err| ResolverError::Other(err.into()))?; + let res = env.resolve_local_module(&dep_path)?; + let ms = ModuleSource { + name: pkg_name.clone(), + version: res.version.clone().expect("Expected version in module"), + source: ModuleSourceKind::Local(dep_path), + }; + // Assert version matches + if let Some(v) = &res.version { + if !req.version.matches(v) { + return Err(ResolverError::LocalDepVersionMismatch( + Box::new(ms), + req.version.clone(), + )); + } + } + + Ok((ms, res)) +} + +fn resolve_pkg_git( + info: &DependencyInfo, + env: &mut ResolverEnv, + pkg_name: &ModuleName, +) -> Result<(ModuleSource, Rc), ResolverError> { + let git_info = GitSource { + url: info.git.clone().unwrap(), + branch: info.git_branch.clone(), + revision: info.git_revision.clone(), + }; + let res = env.resolve_git_module(&git_info, pkg_name)?; + let ms = ModuleSource { + name: pkg_name.clone(), + version: res.version.clone().expect("Expected version in module"), + source: ModuleSourceKind::Git(git_info), + }; + Ok((ms, res)) +} + #[cfg(test)] mod test { use expect_test::expect; diff --git a/crates/mooncake/tests/git_test_template/README.md b/crates/mooncake/tests/git_test_template/README.md new file mode 100644 index 00000000..a263e7d4 --- /dev/null +++ b/crates/mooncake/tests/git_test_template/README.md @@ -0,0 +1 @@ +# testing/test \ No newline at end of file diff --git a/crates/mooncake/tests/git_test_template/lib/hello.mbt b/crates/mooncake/tests/git_test_template/lib/hello.mbt new file mode 100644 index 00000000..9012592a --- /dev/null +++ b/crates/mooncake/tests/git_test_template/lib/hello.mbt @@ -0,0 +1,3 @@ +pub fn hello() -> String { + "Hello, world!" +} diff --git a/crates/mooncake/tests/git_test_template/lib/moon.pkg.json b/crates/mooncake/tests/git_test_template/lib/moon.pkg.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/crates/mooncake/tests/git_test_template/lib/moon.pkg.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/crates/mooncake/tests/git_test_template/moon.mod.json b/crates/mooncake/tests/git_test_template/moon.mod.json new file mode 100644 index 00000000..67d03cee --- /dev/null +++ b/crates/mooncake/tests/git_test_template/moon.mod.json @@ -0,0 +1,9 @@ +{ + "name": "testing/test", + "version": "0.1.0", + "readme": "README.md", + "repository": "", + "license": "Apache-2.0", + "keywords": [], + "description": "" +} \ No newline at end of file diff --git a/crates/mooncake/tests/git_test_template/nonroot_pkg/README.md b/crates/mooncake/tests/git_test_template/nonroot_pkg/README.md new file mode 100644 index 00000000..a263e7d4 --- /dev/null +++ b/crates/mooncake/tests/git_test_template/nonroot_pkg/README.md @@ -0,0 +1 @@ +# testing/test \ No newline at end of file diff --git a/crates/mooncake/tests/git_test_template/nonroot_pkg/lib/hello.mbt b/crates/mooncake/tests/git_test_template/nonroot_pkg/lib/hello.mbt new file mode 100644 index 00000000..9012592a --- /dev/null +++ b/crates/mooncake/tests/git_test_template/nonroot_pkg/lib/hello.mbt @@ -0,0 +1,3 @@ +pub fn hello() -> String { + "Hello, world!" +} diff --git a/crates/mooncake/tests/git_test_template/nonroot_pkg/lib/moon.pkg.json b/crates/mooncake/tests/git_test_template/nonroot_pkg/lib/moon.pkg.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/crates/mooncake/tests/git_test_template/nonroot_pkg/lib/moon.pkg.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/crates/mooncake/tests/git_test_template/nonroot_pkg/moon.mod.json b/crates/mooncake/tests/git_test_template/nonroot_pkg/moon.mod.json new file mode 100644 index 00000000..5afcbb7b --- /dev/null +++ b/crates/mooncake/tests/git_test_template/nonroot_pkg/moon.mod.json @@ -0,0 +1,9 @@ +{ + "name": "testing/nonroot-test", + "version": "0.1.0", + "readme": "README.md", + "repository": "", + "license": "Apache-2.0", + "keywords": [], + "description": "" +} diff --git a/crates/moonutil/Cargo.toml b/crates/moonutil/Cargo.toml index 555abf3c..62b80b0d 100644 --- a/crates/moonutil/Cargo.toml +++ b/crates/moonutil/Cargo.toml @@ -43,6 +43,7 @@ ariadne.workspace = true env_logger.workspace = true log.workspace = true thiserror.workspace = true +twox-hash.workspace = true [dev-dependencies] expect-test.workspace = true diff --git a/crates/moonutil/src/dependency.rs b/crates/moonutil/src/dependency.rs index 6a5ff4ad..5db98a17 100644 --- a/crates/moonutil/src/dependency.rs +++ b/crates/moonutil/src/dependency.rs @@ -33,12 +33,17 @@ pub struct DependencyInfo { /// Local path to the dependency. Overrides the version requirement. #[serde(skip_serializing_if = "Option::is_none")] pub path: Option, - /// Git repository URL. Overrides the version requirement. + + /// Git repository URL. Overrides the version requirement unless the dependency comes from a + /// registry. #[serde(skip_serializing_if = "Option::is_none")] pub git: Option, /// Git branch to use. #[serde(skip_serializing_if = "Option::is_none", rename = "branch")] pub git_branch: Option, + /// Git revision to use. + #[serde(skip_serializing_if = "Option::is_none", rename = "revision")] + pub git_revision: Option, } fn version_is_default(version: &VersionReq) -> bool { @@ -70,7 +75,10 @@ pub enum DependencyInfoJson { impl DependencyInfo { /// Check if the requirement is simple. That is, it only contains a version requirement fn is_simple(&self) -> bool { - self.path.is_none() && self.git.is_none() && self.git_branch.is_none() + self.path.is_none() + && self.git.is_none() + && self.git_branch.is_none() + && self.git_revision.is_none() } #[allow(clippy::needless_update)] // More fields will be added later diff --git a/crates/moonutil/src/hash.rs b/crates/moonutil/src/hash.rs new file mode 100644 index 00000000..c2166217 --- /dev/null +++ b/crates/moonutil/src/hash.rs @@ -0,0 +1,33 @@ +// moon: The build system and package manager for MoonBit. +// Copyright (C) 2024 International Digital Economy Academy +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +// +// For inquiries, you can contact us via e-mail at jichuruanjian@idea.edu.cn. + +use std::hash::{Hash, Hasher}; + +use twox_hash::xxh3; + +/// A 64-bit stable hash of the given data. +pub fn short_hash(data: impl Hash) -> u64 { + let mut hasher = xxh3::Hash64::with_seed(0); + data.hash(&mut hasher); + hasher.finish() +} + +/// A 16-character hexadecimal representation of the hash of the given data. +pub fn short_hash_str(data: impl Hash) -> String { + format!("{:016x}", short_hash(data)) +} diff --git a/crates/moonutil/src/lib.rs b/crates/moonutil/src/lib.rs index 933f0d84..65cfbe17 100644 --- a/crates/moonutil/src/lib.rs +++ b/crates/moonutil/src/lib.rs @@ -24,6 +24,7 @@ pub mod dependency; pub mod dirs; pub mod git; pub mod graph; +pub mod hash; pub mod module; pub mod moon_dir; pub mod mooncake_bin; diff --git a/crates/moonutil/src/moon_dir.rs b/crates/moonutil/src/moon_dir.rs index 34738050..102f301f 100644 --- a/crates/moonutil/src/moon_dir.rs +++ b/crates/moonutil/src/moon_dir.rs @@ -123,6 +123,18 @@ pub fn moon_tmp_dir() -> anyhow::Result { Ok(p) } +pub fn git_dir() -> PathBuf { + home().join("git") +} + +pub fn git_repos_dir() -> PathBuf { + git_dir().join("repos") +} + +pub fn git_checkouts_dir() -> PathBuf { + git_dir().join("checkouts") +} + #[test] fn test_moon_dir() { use expect_test::expect; diff --git a/crates/moonutil/src/mooncakes.rs b/crates/moonutil/src/mooncakes.rs index f5dab4c3..00d58237 100644 --- a/crates/moonutil/src/mooncakes.rs +++ b/crates/moonutil/src/mooncakes.rs @@ -18,6 +18,7 @@ use std::{ collections::HashMap, + fmt::Display, fs::File, io::BufReader, path::{Path, PathBuf}, @@ -89,14 +90,42 @@ impl FromStr for ModuleName { } } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct GitSource { + pub url: String, + pub branch: Option, + pub revision: Option, +} + +impl Display for GitSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.url)?; + if self.branch.is_some() || self.revision.is_some() { + write!(f, " (")?; + let mut first = true; + if let Some(branch) = &self.branch { + write!(f, "branch: {}", branch)?; + first = false; + } + if let Some(revision) = &self.revision { + if !first { + write!(f, ", ")?; + } + write!(f, "revision: {}", revision)?; + } + write!(f, ")")?; + } + Ok(()) + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum ModuleSourceKind { /// Module comes from some registry. If param is `None`, it comes from the default /// registry. Otherwise it comes from a specific registry (unused for now). Registry(Option), // Registry ID? /// Module comes from a git repository. - // TODO: add branch/commit - Git(String), + Git(GitSource), /// Module comes from a local path. The path must be absolute. Local(PathBuf), } @@ -167,11 +196,11 @@ impl ModuleSource { }) } - pub fn git(name: ModuleName, url: String, version: Version) -> Self { + pub fn git(name: ModuleName, details: GitSource, version: Version) -> Self { ModuleSource { name, version, - source: ModuleSourceKind::Git(url), + source: ModuleSourceKind::Git(details), } } } diff --git a/licenserc.toml b/licenserc.toml index 0486f58a..01164bbe 100644 --- a/licenserc.toml +++ b/licenserc.toml @@ -43,4 +43,5 @@ excludes = [ "crates/moon/tests/test_cases/**", "!crates/moon/tests/test_cases/mod.rs", "crates/moonbuild/template/**", + "crates/mooncake/tests/**", ]