diff --git a/src/bin/repo.rs b/src/bin/repo.rs index 25704bb5..877431be 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -4,6 +4,7 @@ use cookbook::config::{CookConfig, get_config, init_config}; use cookbook::cook::cook_build::build; use cookbook::cook::fetch::{fetch, fetch_offline}; use cookbook::cook::fs::{create_target_dir, run_command}; +use cookbook::cook::ident; use cookbook::cook::package::package; use cookbook::cook::pty::{PtyOut, UnixSlavePty, flush_pty, setup_pty}; use cookbook::cook::script::KILL_ALL_PID; @@ -185,6 +186,9 @@ fn main_inner() -> anyhow::Result<()> { } let (config, command, recipe_names) = parse_args(args)?; + if command.is_building() { + ident::init_ident(); + } if command == CliCommand::Cook && config.cook.tui { if let Some((name, e)) = run_tui_cook(config.clone(), recipe_names.clone())? { let _ = stderr().write(e.as_bytes()); @@ -534,10 +538,9 @@ fn handle_fetch( allow_offline: bool, logger: &PtyOut, ) -> anyhow::Result { - let recipe_dir = &recipe.dir; let source_dir = match config.cook.offline && allow_offline { - true => fetch_offline(recipe_dir, &recipe.recipe, logger), - false => fetch(recipe_dir, &recipe.recipe, logger), + true => fetch_offline(&recipe, logger), + false => fetch(&recipe, logger), } .map_err(|e| anyhow!("failed to fetch: {:?}", e))?; @@ -553,7 +556,7 @@ fn handle_cook( ) -> anyhow::Result<()> { let recipe_dir = &recipe.dir; let target_dir = create_target_dir(recipe_dir, recipe.target).map_err(|e| anyhow!(e))?; - let (stage_dir, auto_deps) = build( + let (stage_dirs, auto_deps) = build( recipe_dir, &source_dir, &target_dir, @@ -565,15 +568,8 @@ fn handle_cook( ) .map_err(|err| anyhow!("failed to build: {:?}", err))?; - package( - &stage_dir, - &target_dir, - &recipe.name, - &recipe.recipe, - &auto_deps, - logger, - ) - .map_err(|err| anyhow!("failed to package: {:?}", err))?; + package(&recipe, &stage_dirs, &auto_deps, logger) + .map_err(|err| anyhow!("failed to package: {:?}", err))?; Ok(()) } diff --git a/src/bin/repo_builder.rs b/src/bin/repo_builder.rs index cc090a31..c7b2a9b6 100644 --- a/src/bin/repo_builder.rs +++ b/src/bin/repo_builder.rs @@ -1,10 +1,11 @@ use anyhow::anyhow; use cookbook::WALK_DEPTH; -use cookbook::config::{get_config, init_config}; -use cookbook::cook::package as cook_package; +use cookbook::cook::ident::{get_ident, init_ident}; +use cookbook::cook::{fetch, package as cook_package}; use cookbook::recipe::CookRecipe; +use pkg::package::{Repository, SourceIdentifier}; use pkg::{Package, PackageName, recipes}; -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::env; use std::fs::{self, File}; use std::io::{Read, Write}; @@ -27,7 +28,6 @@ fn is_newer(src: &Path, dst: &Path) -> bool { #[derive(Clone)] struct CliConfig { repo_dir: PathBuf, - nonstop: bool, appstream: bool, recipe_list: Vec, } @@ -40,7 +40,6 @@ impl CliConfig { .expect("Usage: repo_builder ..."); Ok(CliConfig { repo_dir: PathBuf::from(repo_dir), - nonstop: get_config().cook.nonstop, appstream: env::var("COOKBOOK_APPSTREAM").ok().as_deref() == Some("true"), recipe_list: args.collect(), }) @@ -48,7 +47,7 @@ impl CliConfig { } fn main() -> Result<(), Box> { - init_config(); + init_ident(); let conf = CliConfig::parse_args()?; Ok(publish_packages(&conf)?) } @@ -65,7 +64,7 @@ fn publish_packages(config: &CliConfig) -> anyhow::Result<()> { // // The following adds the package dependencies of the recipes to the repo as // well. - let recipe_list = Package::new_recursive( + let (recipe_list, recipe_map) = Package::new_recursive_nonstop( &config .recipe_list .iter() @@ -73,18 +72,16 @@ fn publish_packages(config: &CliConfig) -> anyhow::Result<()> { // Don't publish host packages .filter(|pkg| pkg.as_ref().is_ok_and(|p| !p.is_host())) .collect::, _>>()?, - config.nonstop, WALK_DEPTH, - )? - .into_iter() - .map(|pkg| pkg.name.clone()) - .collect::>(); + ); let mut appstream_sources: HashMap = HashMap::new(); let mut packages: BTreeMap = BTreeMap::new(); + let mut outdated_packages: BTreeMap = BTreeMap::new(); // === 1. Push recipes in list === - for recipe in &recipe_list { + for recipe_toml in &recipe_list { + let recipe = &recipe_toml.name; let Some(recipe_path) = recipes::find(recipe.name()) else { eprintln!("recipe {} not found", recipe); continue; @@ -163,22 +160,68 @@ fn publish_packages(config: &CliConfig) -> anyhow::Result<()> { } } + // === 3. List outdated packages === + for (recipe, e) in recipe_map + .into_iter() + .filter_map(|(k, v)| v.err().and_then(|e| Some((k, e)))) + { + eprintln!( + "\x1b[0;91;49mrepo - marking {} as outdated:\x1b[0m {e}", + recipe + ); + + let Some(recipe_path) = recipes::find(recipe.name()) else { + eprintln!("recipe {} not found", recipe); + continue; + }; + let Ok(cookbook_recipe) = CookRecipe::from_path(recipe_path, true, false) else { + eprintln!("recipe {} unable to read", recipe); + continue; + }; + + match fetch::fetch_get_source_info(&cookbook_recipe) { + Ok(source_ident) => { + outdated_packages.insert(recipe.name().to_string(), source_ident); + } + Err(e) => { + eprintln!( + "\x1b[0;91;49m source of {} is not identifiable:\x1b[0m {e}", + recipe + ); + let ident = get_ident(); + outdated_packages.insert( + recipe.name().to_string(), + SourceIdentifier { + source_identifier: "missing_source".to_string(), + commit_identifier: ident.commit.clone(), + time_identifier: ident.time.clone(), + }, + ); + } + }; + } + eprintln!("\x1b[01;38;5;155mrepo - generating repo.toml\x1b[0m"); - // === 3. Read and update repo.toml === + // === 4. Read and update repo.toml === let repo_toml_path = repo_path.join("repo.toml"); if repo_toml_path.exists() { let mut file = File::open(&repo_toml_path)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; - let parsed: Value = toml::from_str(&contents)?; - if let Some(pkg_table) = parsed.get("packages").and_then(|v| v.as_table()) { - for (k, v) in pkg_table { - if let Some(s) = v.as_str() { - packages.insert(k.clone(), format!("\"{}\"", s)); - } else { - packages.insert(k.clone(), v.to_string()); + let parsed: Repository = toml::from_str(&contents)?; + for (k, v) in parsed.packages { + packages.insert(k, v); + } + if parsed.outdated_packages.len() > 0 { + let built_packages: BTreeSet = recipe_list + .iter() + .map(|p| p.name.name().to_string()) + .collect(); + for (k, v) in parsed.outdated_packages { + if outdated_packages.contains_key(&k) || !built_packages.contains(&k) { + outdated_packages.insert(k, v); } } } @@ -203,21 +246,16 @@ fn publish_packages(config: &CliConfig) -> anyhow::Result<()> { let version_str = parsed .get("blake3") .unwrap_or_else(|| parsed.get("version").unwrap_or_else(|| &empty_ver)) - .to_string(); // includes quotes + .as_str() + .unwrap_or(""); let package_name = path.file_stem().unwrap().to_string_lossy().to_string(); - packages.insert(package_name, version_str); - } - - // FIXME: Use proper TOML serializer - let mut output = String::from("[packages]\n"); - for (name, version) in &packages { - output.push_str(&if name.contains('.') { - format!("\"{name}\" = {version}\n") - } else { - format!("{name} = {version}\n") - }); + packages.insert(package_name, version_str.to_string()); } + let output = toml::to_string(&Repository { + packages, + outdated_packages, + })?; let mut output_file = File::create(&repo_toml_path)?; output_file.write_all(output.as_bytes())?; diff --git a/src/cook.rs b/src/cook.rs index d0946e0a..70a53a06 100644 --- a/src/cook.rs +++ b/src/cook.rs @@ -2,6 +2,7 @@ pub mod cook_build; pub mod fetch; pub mod fs; +pub mod ident; pub mod package; pub mod pty; pub mod script; diff --git a/src/cook/cook_build.rs b/src/cook/cook_build.rs index 74d515c9..365fb58a 100644 --- a/src/cook/cook_build.rs +++ b/src/cook/cook_build.rs @@ -265,6 +265,15 @@ pub fn build( if stage_dir.is_dir() { remove_all(&stage_dir)?; } + // delete to let repo_builder know if this build fail later + let stage_file = stage_dir.with_added_extension("pkgar"); + if stage_file.is_file() { + remove_all(&stage_file)?; + } + let stage_meta = stage_dir.with_added_extension("toml"); + if stage_meta.is_file() { + remove_all(&stage_meta)?; + } } } } diff --git a/src/cook/fetch.rs b/src/cook/fetch.rs index 9bbef81a..5c31a6a8 100644 --- a/src/cook/fetch.rs +++ b/src/cook/fetch.rs @@ -1,3 +1,5 @@ +use pkg::package::SourceIdentifier; + use crate::REMOTE_PKG_SOURCE; use crate::config::translate_mirror; use crate::cook::fs::*; @@ -8,9 +10,11 @@ use crate::cook::script::*; use crate::is_redox; use crate::log_to_pty; use crate::recipe::BuildKind; -use crate::recipe::Recipe; +use crate::recipe::CookRecipe; use crate::{blake3, recipe::SourceRecipe}; use std::fs; +use std::fs::File; +use std::io::Read; use std::path::{Path, PathBuf}; use std::process::Command; @@ -30,27 +34,33 @@ pub(crate) fn get_blake3(path: &PathBuf, show_progress: bool) -> Result Result { +pub fn fetch_offline(recipe: &CookRecipe, logger: &PtyOut) -> Result { + let recipe_dir = &recipe.dir; let source_dir = recipe_dir.join("source"); - if recipe.build.kind == BuildKind::None { - // the build function doesn't need source dir exists - return Ok(source_dir); - } - if recipe.build.kind == BuildKind::Remote { - fetch_remote(recipe_dir, recipe, true, logger)?; - return Ok(source_dir); + match recipe.recipe.build.kind { + BuildKind::None => { + // the build function doesn't need source dir exists + fetch_apply_source_info(recipe, "".to_string())?; + return Ok(source_dir); + } + BuildKind::Remote => { + fetch_remote(recipe_dir, recipe, true, logger)?; + return Ok(source_dir); + } + _ => {} } - match &recipe.source { + let ident = match &recipe.recipe.source { Some(SourceRecipe::Path { path: _ }) | None => { - return fetch(recipe_dir, recipe, logger); + fetch(recipe, logger)?; + "local_source".to_string() } - Some(SourceRecipe::SameAs { same_as: _ }) => { - return fetch(recipe_dir, recipe, logger); + Some(SourceRecipe::SameAs { same_as }) => { + let recipe = fetch_resolve_canon(recipe_dir, &same_as, recipe.name.is_host())?; + // recursively fetch + fetch_offline(&recipe, logger)?; + fetch_make_symlink(&source_dir, &same_as)?; + fetch_get_source_info(&recipe)?.source_identifier } Some(SourceRecipe::Git { git: _, @@ -62,6 +72,8 @@ pub fn fetch_offline( shallow_clone: _, }) => { offline_check_exists(&source_dir)?; + let (head_rev, _) = get_git_head_rev(&source_dir)?; + head_rev } Some(SourceRecipe::Tar { tar: _, @@ -93,29 +105,38 @@ pub fn fetch_offline( offline_check_exists(&source_dir)?; } } + blake3.clone().unwrap_or("no_tar_blake3_hash_info".into()) } - } + }; + + fetch_apply_source_info(recipe, ident)?; Ok(source_dir) } -pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &PtyOut) -> Result { +pub fn fetch(recipe: &CookRecipe, logger: &PtyOut) -> Result { + let recipe_dir = &recipe.dir; let source_dir = recipe_dir.join("source"); - if recipe.build.kind == BuildKind::None { - // the build function doesn't need source dir exists - return Ok(source_dir); - } - if recipe.build.kind == BuildKind::Remote { - fetch_remote(recipe_dir, recipe, false, logger)?; - return Ok(source_dir); + match recipe.recipe.build.kind { + BuildKind::None => { + // the build function doesn't need source dir exists + fetch_apply_source_info(recipe, "".to_string())?; + return Ok(source_dir); + } + BuildKind::Remote => { + fetch_remote(recipe_dir, recipe, false, logger)?; + return Ok(source_dir); + } + _ => {} } - match &recipe.source { + let ident = match &recipe.recipe.source { Some(SourceRecipe::SameAs { same_as }) => { - let (canon_dir, recipe) = fetch_resolve_canon(recipe_dir, &same_as)?; + let recipe = fetch_resolve_canon(recipe_dir, &same_as, recipe.name.is_host())?; // recursively fetch - fetch(&canon_dir, &recipe, logger)?; + fetch(&recipe, logger)?; fetch_make_symlink(&source_dir, &same_as)?; + fetch_get_source_info(&recipe)?.source_identifier } Some(SourceRecipe::Path { path }) => { if !source_dir.is_dir() || modified_dir(Path::new(&path))? > modified_dir(&source_dir)? @@ -135,6 +156,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &PtyOut) -> Result Result { //TODO: use libgit? let shallow_clone = *shallow_clone == Some(true); - if !source_dir.is_dir() { + let can_skip_rebuild = if !source_dir.is_dir() { // Create source.tmp let source_dir_tmp = recipe_dir.join("source.tmp"); create_dir_clean(&source_dir_tmp)?; @@ -171,9 +193,10 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &PtyOut) -> Result Result /dev/null || - // git remote add upstream "$GIT_UPSTREAM" - // git fetch upstream - } + let (head_rev, detached_rev) = get_git_head_rev(&source_dir)?; + if detached_rev { + if let Some(rev) = rev + && let Ok(exp_rev) = get_git_tag_rev(&source_dir, &rev) + { + exp_rev == head_rev + } else { + false + } + } else { + let (_, remote_branch, remote_name, remote_url) = + get_git_remote_tracking(&source_dir)?; + // TODO: how to get default branch and compare it here? + if remote_name == "origin" && &remote_url == chop_dot_git(git) { + let fetch_rev = + get_git_fetch_rev(&source_dir, &remote_url, &remote_branch)?; + fetch_rev == head_rev + } else { + false + } + } + }; - if let Some(rev) = rev { - // Check out specified revision - let mut command = Command::new("git"); - command.arg("-C").arg(&source_dir); - command.arg("checkout").arg(rev); - run_command(command, logger)?; - } else if !is_redox() { - //If patches exists, we have to drop it - if patches.len() > 0 { + if !can_skip_rebuild { + if let Some(_upstream) = upstream { + //TODO: set upstream URL (is this needed?) + // git remote set-url upstream "$GIT_UPSTREAM" &> /dev/null || + // git remote add upstream "$GIT_UPSTREAM" + // git fetch upstream + } + + if let Some(rev) = rev { + // Check out specified revision + let mut command = Command::new("git"); + command.arg("-C").arg(&source_dir); + command.arg("checkout").arg(rev); + run_command(command, logger)?; + } else if !is_redox() { + //If patches exists, we have to drop it + if patches.len() > 0 { + let mut command = Command::new("git"); + command.arg("-C").arg(&source_dir); + command.arg("reset").arg("--hard"); + run_command(command, logger)?; + } + //TODO: complicated stuff to check and reset branch to origin + //TODO: redox can't undestand this (got exit status 1) + let mut command = Command::new("bash"); + command.arg("-c").arg(GIT_RESET_BRANCH); + if let Some(branch) = branch { + command.env("BRANCH", branch); + } + command.current_dir(&source_dir); + run_command(command, logger)?; + } + + if !patches.is_empty() || script.is_some() { + // Hard reset let mut command = Command::new("git"); command.arg("-C").arg(&source_dir); command.arg("reset").arg("--hard"); run_command(command, logger)?; } - //TODO: complicated stuff to check and reset branch to origin - //TODO: redox can't undestand this (got exit status 1) - let mut command = Command::new("bash"); - command.arg("-c").arg(GIT_RESET_BRANCH); - if let Some(branch) = branch { - command.env("BRANCH", branch); - } - command.current_dir(&source_dir); - run_command(command, logger)?; - } - if !patches.is_empty() || script.is_some() { - // Hard reset + // Sync submodules URL let mut command = Command::new("git"); command.arg("-C").arg(&source_dir); - command.arg("reset").arg("--hard"); + command.arg("submodule").arg("sync").arg("--recursive"); + run_command(command, logger)?; + + // Update submodules + let mut command = Command::new("git"); + command.arg("-C").arg(&source_dir); + command + .arg("submodule") + .arg("update") + .arg("--init") + .arg("--recursive"); + if shallow_clone { + command.arg("--filter=tree:0"); + } + run_command(command, logger)?; + + fetch_apply_patches(recipe_dir, patches, script, &source_dir, logger)?; } - // Sync submodules URL - let mut command = Command::new("git"); - command.arg("-C").arg(&source_dir); - command.arg("submodule").arg("sync").arg("--recursive"); - - run_command(command, logger)?; - - // Update submodules - let mut command = Command::new("git"); - command.arg("-C").arg(&source_dir); - command - .arg("submodule") - .arg("update") - .arg("--init") - .arg("--recursive"); - if shallow_clone { - command.arg("--filter=tree:0"); - } - run_command(command, logger)?; - - fetch_apply_patches(recipe_dir, patches, script, &source_dir, logger)?; + let (head_rev, _) = get_git_head_rev(&source_dir)?; + head_rev } Some(SourceRecipe::Tar { tar, @@ -316,6 +366,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &PtyOut) -> Result { @@ -327,17 +378,20 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &PtyOut) -> Result Resu pub(crate) fn fetch_resolve_canon( recipe_dir: &Path, same_as: &String, -) -> Result<(PathBuf, Recipe), String> { + is_host: bool, +) -> Result { let canon_dir = Path::new(recipe_dir).join(same_as); if canon_dir .to_str() @@ -382,12 +437,8 @@ pub(crate) fn fetch_resolve_canon( if !canon_dir.exists() { return Err(format!("'{dir}' is not exists.", dir = canon_dir.display())); } - let recipe_path = canon_dir.join("recipe.toml"); - let recipe_str = fs::read_to_string(&recipe_path) - .map_err(|e| format!("unable to read {path}: {e}", path = recipe_path.display()))?; - let recipe: Recipe = toml::from_str(&recipe_str) - .map_err(|e| format!("Unable to parse {path}: {e}", path = recipe_path.display()))?; - Ok((canon_dir, recipe)) + CookRecipe::from_path(canon_dir.as_path(), true, is_host) + .map_err(|e| format!("Unable to load {dir}: {e:?}", dir = canon_dir.display())) } pub(crate) fn fetch_extract_tar( @@ -455,13 +506,11 @@ fn get_pubkey_url() -> String { pub fn fetch_remote( recipe_dir: &Path, - recipe: &Recipe, + recipe: &CookRecipe, offline_mode: bool, logger: &PtyOut, ) -> Result<(), String> { - // TODO: allow download to host target (waiting for build server to have them) - let target = redoxer::target(); - let target_dir = create_target_dir(recipe_dir, target)?; + let target_dir = create_target_dir(recipe_dir, recipe.target)?; let source_pubkey = target_dir.join("id_ed25519.pub.toml"); if !offline_mode { download_wget(&get_pubkey_url(), &source_pubkey, logger)?; @@ -469,7 +518,7 @@ pub fn fetch_remote( offline_check_exists(&source_pubkey)?; } - let packages = recipe.get_packages_list(); + let packages = recipe.recipe.get_packages_list(); let name = recipe_dir .file_name() @@ -493,6 +542,28 @@ pub fn fetch_remote( offline_check_exists(&source_pkgar)?; offline_check_exists(&source_toml)?; } + + // guaranteed to exist once + if package.is_none() { + let mut file = File::open(&source_toml) + .map_err(|e| format!("Unable to open source.toml: {e:?}"))?; + let mut contents = String::new(); + file.read_to_string(&mut contents) + .map_err(|e| format!("Unable to read source.toml: {e:?}"))?; + + let pkg_toml = pkg::Package::from_toml(&contents) + .map_err(|e| format!("Unable to parse source.toml: {e:?}"))?; + + fetch_apply_source_info_from_remote( + recipe, + &SourceIdentifier { + commit_identifier: pkg_toml.commit_identifier.clone(), + source_identifier: pkg_toml.source_identifier.clone(), + time_identifier: pkg_toml.time_identifier.clone(), + ..Default::default() + }, + )?; + } } Ok(()) @@ -563,3 +634,37 @@ pub(crate) fn fetch_apply_patches( )?; }) } + +pub(crate) fn fetch_apply_source_info( + recipe: &CookRecipe, + source_identifier: String, +) -> Result<(), String> { + let ident = crate::cook::ident::get_ident(); + let info = pkg::package::SourceIdentifier { + commit_identifier: ident.commit.to_string(), + time_identifier: ident.time.to_string(), + source_identifier: source_identifier, + }; + + fetch_apply_source_info_from_remote(&recipe, &info) +} + +pub(crate) fn fetch_apply_source_info_from_remote( + recipe: &CookRecipe, + info: &pkg::package::SourceIdentifier, +) -> Result<(), String> { + let target_dir = create_target_dir(&recipe.dir, recipe.target)?; + let source_toml_path = target_dir.join("source_info.toml"); + serialize_and_write(&source_toml_path, &info)?; + Ok(()) +} + +pub fn fetch_get_source_info(recipe: &CookRecipe) -> Result { + let target_dir = recipe.target_dir(); + let source_toml_path = target_dir.join("source_info.toml"); + let toml_content = fs::read_to_string(source_toml_path) + .map_err(|e| format!("Unable to read source_info.toml: {:?}", e))?; + let parsed = toml::from_str(&toml_content) + .map_err(|e| format!("Unable to parse source_info.toml: {:?}", e))?; + Ok(parsed) +} diff --git a/src/cook/fs.rs b/src/cook/fs.rs index f5de8bbd..b46ec5e0 100644 --- a/src/cook/fs.rs +++ b/src/cook/fs.rs @@ -285,3 +285,186 @@ pub fn download_wget(url: &str, dest: &PathBuf, logger: &PtyOut) -> Result<(), S } Ok(()) } + +/// get commit rev and return if it's detached or not +pub fn get_git_head_rev(dir: &PathBuf) -> Result<(String, bool), String> { + let git_head = dir.join(".git/HEAD"); + let head_str = fs::read_to_string(&git_head) + .map_err(|e| format!("unable to read {path}: {e}", path = git_head.display()))?; + if head_str.starts_with("ref: ") { + let git_ref = dir.join(".git").join(head_str["ref: ".len()..].trim_end()); + let ref_str = fs::read_to_string(&git_ref) + .map_err(|e| format!("unable to read {path}: {e}", path = git_ref.display()))?; + Ok((ref_str.trim().to_string(), false)) + } else { + Ok((head_str.trim().to_string(), true)) + } +} + +pub fn get_git_tag_rev(dir: &PathBuf, tag: &str) -> Result { + if tag.len() == 40 && tag.chars().all(|f| f.is_ascii_hexdigit()) { + return Ok(tag.to_string()); + } + let git_refs = dir.join(".git/packed-refs"); + let refs_str = fs::read_to_string(&git_refs) + .map_err(|e| format!("unable to read {path}: {e}", path = git_refs.display()))?; + let expected_comment_part = format!("refs/tags/{tag}"); + for line in refs_str.lines() { + if line.contains(&expected_comment_part) { + let sha = line + .split_whitespace() + .next() + .ok_or_else(|| "packed-refs line is malformed.".to_string())?; + + return Ok(sha.to_string()); + } + } + + Err(format!( + "Could not find a rev tag for {}", + expected_comment_part + )) +} + +/// get commit rev after fetch +pub fn get_git_fetch_rev( + dir: &PathBuf, + remote_url: &str, + remote_branch: &str, +) -> Result { + let git_fetch_head = dir.join(".git/FETCH_HEAD"); + + let fetch_head_content = fs::read_to_string(&git_fetch_head).map_err(|e| { + format!( + "unable to read {path}: {e}", + path = git_fetch_head.display() + ) + })?; + + let expected_comment_part = format!("branch '{}' of {}", remote_branch, remote_url); + + for line in fetch_head_content.lines() { + if line.contains(&expected_comment_part) && !line.contains("not-for-merge") { + let sha = line + .split_whitespace() + .next() + .ok_or_else(|| "FETCH_HEAD line is malformed.".to_string())?; + + return Ok(sha.to_string()); + } + } + + Err(format!( + "Could not find a fetch target for tracking {}", + expected_comment_part + )) +} + +/// (local_branch_name, remote_branch, remote_name, remote_url) +/// -> ("fix_stuff", "master", "origin", "https://gitlab.redox-os.org/willnode/redox") +pub fn get_git_remote_tracking(dir: &PathBuf) -> Result<(String, String, String, String), String> { + let git_head = dir.join(".git/HEAD"); + let git_config = dir.join(".git/config"); + + let head_content = fs::read_to_string(&git_head) + .map_err(|e| format!("unable to read {path}: {e}", path = git_head.display()))?; + + if !head_content.starts_with("ref: ") { + let sha = head_content.trim_end().to_string(); + return Ok((sha, "".to_string(), "".to_string(), "".to_string())); + } + + let local_branch_path = head_content["ref: ".len()..].trim_end(); + let local_branch_name = get_git_branch_name(local_branch_path)?; + + let config_content = fs::read_to_string(&git_config) + .map_err(|e| format!("unable to read {path}: {e}", path = git_config.display()))?; + + let branch_section = format!("[branch \"{}\"]", local_branch_name); + let mut remote_name: Option = None; + let mut remote_branch: Option = None; + let mut parsing_branch_section = false; + + for line in config_content.lines().map(|l| l.trim()) { + if line.is_empty() { + continue; + } + + if line == branch_section { + parsing_branch_section = true; + continue; + } + + if parsing_branch_section { + if line.starts_with('[') { + break; + } + if line.starts_with("remote = ") { + remote_name = Some(line["remote = ".len()..].trim().to_string()); + } + if line.starts_with("merge = ") { + remote_branch = Some(get_git_branch_name(line["merge = ".len()..].trim())?); + } + } + } + + let remote_name_str = remote_name + .ok_or_else(|| format!("Branch '{}' is not tracking a remote.", local_branch_name))?; + let remote_branch_str = remote_branch.unwrap_or("".into()); + + let remote_section = format!("[remote \"{}\"]", remote_name_str); + let mut remote_url: Option = None; + let mut parsing_remote_section = false; + + for line in config_content.lines().map(|l| l.trim()) { + if line.is_empty() { + continue; + } + + if line == remote_section { + parsing_remote_section = true; + continue; + } + + if parsing_remote_section { + if line.starts_with('[') { + break; + } + if line.starts_with("url = ") { + let mut url = line["url = ".len()..].trim(); + url = chop_dot_git(url); + remote_url = Some(url.to_string()); + } + } + } + + let remote_url_str = remote_url.ok_or_else(|| { + format!( + "Could not find URL for remote '{}' in .git/config.", + remote_name_str + ) + })?; + + Ok(( + local_branch_name, + remote_branch_str, + remote_name_str, + remote_url_str, + )) +} + +pub(crate) fn chop_dot_git(url: &str) -> &str { + if url.ends_with(".git") { + return &url[..url.len() - ".git".len()]; + } + url +} + +fn get_git_branch_name(local_branch_path: &str) -> Result { + // TODO: incorrectly handle branch with slashes + Ok(local_branch_path + .split('/') + .last() + .ok_or_else(|| format!("Failed to parse branch name of {:?}", local_branch_path))? + .to_string()) +} diff --git a/src/cook/ident.rs b/src/cook/ident.rs new file mode 100644 index 00000000..85359956 --- /dev/null +++ b/src/cook/ident.rs @@ -0,0 +1,46 @@ +use std::{ + process::{Command, Stdio}, + sync::OnceLock, +}; + +#[derive(Debug, Default)] +pub struct IdentifierConfig { + pub commit: String, + pub time: String, +} + +impl IdentifierConfig { + fn new() -> Self { + let (commit, _) = crate::cook::fs::get_git_head_rev( + &std::env::current_dir().expect("unable to get $PWD"), + ) + .expect("Can't read this repository commit"); + // better than importing heavy deps like chrono + let time = String::from_utf8_lossy( + &Command::new("date") + .arg("-u") + .arg("+%Y-%m-%dT%H:%M:%SZ") + .stdout(Stdio::piped()) + .output() + .expect("Failed to get current ISO-formatted time") + .stdout + .trim_ascii(), + ) + .into(); + IdentifierConfig { commit, time } + } +} + +static IDENTIFIER_CONFIG: OnceLock = OnceLock::new(); + +pub fn get_ident() -> &'static IdentifierConfig { + IDENTIFIER_CONFIG + .get() + .expect("Identifier is not initialized") +} + +pub fn init_ident() { + IDENTIFIER_CONFIG + .set(IdentifierConfig::new()) + .expect("Identifier is initialized twice") +} diff --git a/src/cook/package.rs b/src/cook/package.rs index bfbf5181..3a635377 100644 --- a/src/cook/package.rs +++ b/src/cook/package.rs @@ -7,27 +7,26 @@ use pkg::{Package, PackageName}; use crate::{ blake3::hash_to_hex, - cook::{fs::*, pty::PtyOut}, + cook::{fetch, fs::*, pty::PtyOut}, log_to_pty, - recipe::{BuildKind, OptionalPackageRecipe, Recipe}, + recipe::{BuildKind, CookRecipe, OptionalPackageRecipe, Recipe}, }; pub fn package( + recipe: &CookRecipe, stage_dirs: &Vec, - target_dir: &Path, - name: &PackageName, - recipe: &Recipe, auto_deps: &BTreeSet, logger: &PtyOut, ) -> Result<(), String> { - if recipe.build.kind == BuildKind::None { + let name = &recipe.name; + let target_dir = &recipe.target_dir(); + if recipe.recipe.build.kind == BuildKind::None { // metapackages don't have stage dir and optional packages package_toml( target_dir.join("stage.toml"), - name, recipe, None, - recipe.package.dependencies.clone(), + recipe.recipe.package.dependencies.clone(), &auto_deps, )?; return Ok(()); @@ -50,7 +49,7 @@ pub fn package( let stage_modified = modified_all(stage_dirs, modified_dir)?; - let packages = recipe.get_packages_list(); + let packages = recipe.recipe.get_packages_list(); for package in packages { let (stage_dir, package_file, package_meta) = package_stage_paths(package, target_dir); @@ -96,11 +95,10 @@ pub fn package( } }) .collect(), - None => recipe.package.dependencies.clone(), + None => recipe.recipe.package.dependencies.clone(), }; package_toml( package_meta, - &name, recipe, Some((Path::new(public_path), &package_file)), package_deps, @@ -114,8 +112,7 @@ pub fn package( pub fn package_toml( toml_path: PathBuf, - name: &PackageName, - recipe: &Recipe, + recipe: &CookRecipe, package_file: Option<(&Path, &PathBuf)>, mut package_deps: Vec, auto_deps: &BTreeSet, @@ -148,15 +145,21 @@ pub fn package_toml( ("".into(), 0) }; + let ident_source = fetch::fetch_get_source_info(recipe)?; + let package = Package { - name: name.without_host(), - version: package_version(recipe), - target: package_target(name).to_string(), + name: recipe.name.without_host(), + version: package_version(&recipe.recipe), + target: recipe.target.to_string(), blake3: hash, // this size will be different once pkgar supports compression network_size: size, storage_size: size, depends: package_deps, + commit_identifier: ident_source.commit_identifier, + source_identifier: ident_source.source_identifier, + time_identifier: ident_source.time_identifier, + ..Default::default() }; serialize_and_write(&toml_path, &package)?;