Merge branch 'capture-rev' into 'master'

Implement build system rollback

Closes #1799

See merge request redox-os/redox!2137
This commit is contained in:
Jeremy Soller 2026-05-10 06:53:11 -06:00
commit 0acb0392b9
5 changed files with 169 additions and 51 deletions

View File

@ -47,6 +47,14 @@ else
$(REPO_BIN) fetch $(COOKBOOK_OPTS) --with-package-deps
endif
# Unfetch and clean all recipes source or binary from filesystem config
unfetch: prefix $(FSTOOLS_TAG) FORCE
ifeq ($(PODMAN_BUILD),1)
$(PODMAN_RUN) make $@
else
$(REPO_BIN) unfetch $(COOKBOOK_OPTS) --with-package-deps
endif
# Fetch Cargo dependencies for the cookbook tool (needed for REPO_OFFLINE=1 builds)
cargo-fetch: FORCE
ifeq ($(PODMAN_BUILD),1)
@ -281,7 +289,7 @@ cc.%: $(FSTOOLS_TAG) FORCE
ifeq ($(PODMAN_BUILD),1)
$(PODMAN_RUN) make $@
else
$(REPO_BIN) change-rule --set-rule= $(foreach f,$(subst $(comma), ,$*),$(f)) --with-package-deps
$(REPO_BIN) change-rule --unset $(foreach f,$(subst $(comma), ,$*),$(f)) --with-package-deps
endif
# Set recipe rule to "binary" then invoke clean and rebuild
@ -294,6 +302,37 @@ scr.%: $(FSTOOLS_TAG) FORCE
$(MAKE) sc.$*
$(MAKE) r.$*,--with-package-deps
# Save current git rev for next recipe fetch, locking git recipes frozen in time
repo-lock: $(FSTOOLS_TAG) FORCE
ifeq ($(PODMAN_BUILD),1)
$(PODMAN_RUN) make $@
else
$(REPO_BIN) capture-rev $(COOKBOOK_OPTS) --with-package-deps
endif
# Undo repo-lock, allowing git recipes to get updated by next recipe fetch
repo-unlock: $(FSTOOLS_TAG) FORCE
ifeq ($(PODMAN_BUILD),1)
$(PODMAN_RUN) make $@
else
$(REPO_BIN) capture-rev $(COOKBOOK_OPTS) --unset --with-package-deps
endif
# Like repo-lock, but also checking out the specified git rev.
# Revert this operation by "git checkout master" "make pull" then "make repo-unlock".
# Will not work if rolling back to a commit before 2026.
repo-rollback.%: $(FSTOOLS_TAG) FORCE
ifeq ($(PODMAN_BUILD),1)
$(PODMAN_RUN) make $@
# have to be done otherwise podman will rebuild
touch $(CONTAINER_TAG)
else
git checkout $*
$(REPO_BIN) capture-rev $(COOKBOOK_OPTS) --rollback --with-package-deps
endif
# have to be done otherwise cookbook will rebuild
touch $(FSTOOLS_TAG)
export DEBUG_BIN?=
# Debug a statically linked program with gdbgui, for example: debug.drivers-initfs DEBUG_BIN=pcid

View File

@ -2,7 +2,10 @@ use ansi_to_tui::IntoText;
use cookbook::config::{CookConfig, CookLockOpt, get_config, init_config};
use cookbook::cook::cook_build::{build, get_stage_dirs, remove_stage_dir};
use cookbook::cook::fetch::{FetchResult, fetch, fetch_offline};
use cookbook::cook::fs::{create_dir, create_target_dir, remove_all, run_command};
use cookbook::cook::fs::{
create_dir, create_target_dir, get_git_commit_date, get_git_head_rev, get_git_rev_before_date,
remove_all, run_command,
};
use cookbook::cook::package::{package, package_handle_push};
use cookbook::cook::pty::{PtyOut, UnixSlavePty, flush_pty, setup_pty, write_to_pty};
use cookbook::cook::script::KILL_ALL_PID;
@ -58,15 +61,15 @@ const REPO_HELP_STR: &str = r#"
common flags:
--cookbook=<cookbook_dir> the "recipes" folder, default to $PWD/recipes
--repo=<repo_dir> the "repo" folder, default to $PWD/repo
--sysroot=<sysroot_dir> the "root" folder used for "push" command
For Redox, defaults to "/", else default to $PWD/sysroot
--with-package-deps include package deps (always implied in push command)
--all apply to all recipes in <cookbook_dir>
--category=<category> apply to all recipes in <cookbook_dir>/<category>
--filesystem=<filesystem> override recipes config using installer file
--repo-binary override recipes config to use repo_binary
--sysroot=<sysroot_dir> used in "push", the "root" dir, default to $PWD/sysroot
--set-rule=<rule> used in "change-rule", set wanted config rule
--rollback=<rev> used in "capture-rev", rollback to a commit instead
--rollback used in "capture-rev", allow git to rollback
--unset used in "capture-rev" and "change-rule", unset locks
cook env and their defaults:
CI= set to any value to disable TUI
@ -91,8 +94,9 @@ struct CliConfig {
logs_dir: Option<PathBuf>,
category: Option<PathBuf>,
filesystem: Option<redox_installer::Config>,
rollback: Option<String>,
set_rule: Option<String>,
unset: bool,
with_rollback: bool,
with_package_deps: bool,
all: bool,
cook: CookConfig,
@ -187,16 +191,13 @@ impl CliConfig {
None
},
category: None,
sysroot_dir: if cfg!(target_os = "redox") {
PathBuf::from("/")
} else {
current_dir.join("sysroot")
},
sysroot_dir: current_dir.join("sysroot"),
with_package_deps: false,
cook: get_config().cook.clone(),
all: false,
unset: false,
filesystem: None,
rollback: None,
with_rollback: false,
set_rule: None,
})
}
@ -262,11 +263,8 @@ fn main_inner() -> Result<()> {
if command == CliCommand::Push {
return handle_push(&recipes, &config);
}
if command == CliCommand::ChangeRule {
return handle_change_rule(&recipes, &config);
}
if command == CliCommand::CaptureRev {
return handle_capture_rev(&recipes, &config);
if matches!(command, CliCommand::ChangeRule | CliCommand::CaptureRev) {
return handle_change_rule(&recipes, &config, &command);
}
let verbose = config.cook.verbose;
@ -454,7 +452,6 @@ fn parse_args(args: Vec<String>) -> Result<(CliConfig, CliCommand, Vec<CookRecip
"--sysroot" => config.sysroot_dir = PathBuf::from(value),
"--category" => config.category = Some(PathBuf::from(value)),
"--set-rule" => config.set_rule = Some(value.into()),
"--rollback" => config.rollback = Some(value.into()),
"--filesystem" => {
config.filesystem = Some({
let r = redox_installer::Config::from_file(&PathBuf::from(value));
@ -470,6 +467,8 @@ fn parse_args(args: Vec<String>) -> Result<(CliConfig, CliCommand, Vec<CookRecip
match arg.as_str() {
"--repo-binary" => override_filesystem_repo_binary = true,
"--with-package-deps" => config.with_package_deps = true,
"--rollback" => config.with_rollback = true,
"--unset" => config.unset = true,
"--all" => config.all = true,
_ => bail_options_err!("Error: Unknown flag: {}", arg),
}
@ -718,7 +717,7 @@ fn parse_args(args: Vec<String>) -> Result<(CliConfig, CliCommand, Vec<CookRecip
.flatten()
{
if let Some(SourceRecipe::Git { rev, branch, .. }) = &mut recipe.recipe.source {
*rev = Some(gitrev);
*rev = Some(gitrev.clone());
*branch = None;
} else {
println!(
@ -726,6 +725,7 @@ fn parse_args(args: Vec<String>) -> Result<(CliConfig, CliCommand, Vec<CookRecip
recipe.name.as_str()
);
}
recipe.pinned = true;
}
}
}
@ -968,53 +968,87 @@ fn handle_tree(recipes: &Vec<CookRecipe>, is_build_tree: bool, _config: &CliConf
Ok(())
}
fn handle_change_rule(recipes: &Vec<CookRecipe>, config: &CliConfig) -> Result<()> {
fn handle_change_rule(
recipes: &Vec<CookRecipe>,
config: &CliConfig,
command: &CliCommand,
) -> Result<()> {
let mut lock = get_config().recipe_lock.clone();
let is_pruning = config.set_rule.as_ref().is_some_and(|s| s.is_empty());
let cookbook_date = get_git_commit_date(&PathBuf::from("."))?;
let is_change_rule = matches!(command, CliCommand::ChangeRule);
let is_capture_rev = matches!(command, CliCommand::CaptureRev);
for recipe in recipes {
if recipe.name.is_host() {
if is_change_rule && recipe.name.is_host() {
// host packages will always be "source" so it's pointless to change their rule
continue;
}
if is_capture_rev && !matches!(recipe.recipe.source, Some(SourceRecipe::Git { .. })) {
continue;
}
let recipe_name = recipe.name.without_prefix();
let mut recipe_lock = lock.get(recipe_name).cloned().unwrap_or_default();
let cached = if is_pruning {
recipe_lock.fsrule.take().is_none()
} else {
let new_rule = config
.set_rule
.as_ref()
.cloned()
.unwrap_or_else(|| recipe.rule.clone());
let cached = if is_change_rule {
if config.unset {
recipe_lock.fsrule.take().is_none()
} else {
let new_rule = config
.set_rule
.as_ref()
.cloned()
.unwrap_or_else(|| recipe.rule.clone());
let old_rule = recipe_lock.fsrule.replace(new_rule.clone());
old_rule == Some(new_rule)
let old_rule = recipe_lock.fsrule.replace(new_rule.clone());
old_rule == Some(new_rule)
}
} else if is_capture_rev {
if config.unset {
recipe_lock.gitrev.take().is_none()
} else {
let source_dir = recipe.dir.join("source");
let rev = if config.with_rollback {
// invoke fetch as the git tracking can be different
match handle_fetch(recipe, config, false, &None) {
Ok(_) => get_git_rev_before_date(&source_dir, &cookbook_date),
Err(e) => Err(e),
}
} else {
get_git_head_rev(&source_dir).map(|r| r.0)
};
match rev {
Ok(rev) => {
let old_rev = recipe_lock.gitrev.replace(rev.clone());
old_rev == Some(rev)
}
Err(e) => {
eprintln!("Skipping {}: {e}", recipe.name.as_str());
continue;
}
}
}
} else {
unreachable!()
};
if recipe_lock.is_empty() {
lock.remove(recipe_name);
} else {
lock.insert(recipe_name.to_string(), recipe_lock);
}
let clean_cached = if !cached {
let clean_cached = if !cached && is_change_rule {
handle_clean(recipe, config, &CliCommand::Clean)?
} else {
true
};
if cached && clean_cached {
print_cached(&CliCommand::ChangeRule, &recipe.name);
print_cached(command, &recipe.name);
} else {
print_success(&CliCommand::ChangeRule, &recipe.name);
print_success(command, &recipe.name);
}
}
CookLockOpt { recipes: lock }.save();
Ok(())
}
fn handle_capture_rev(_recipes: &Vec<CookRecipe>, _config: &CliConfig) -> Result<()> {
todo!()
}
//
// ------------- TUI SPECIFIC CODE -------------------
//

View File

@ -186,6 +186,7 @@ pub fn fetch(recipe: &CookRecipe, check_source: bool, logger: &PtyOut) -> Result
}) => {
//TODO: use libgit?
let shallow_clone = *shallow_clone == Some(true);
let mut fetch_is_ran = false;
let cached = if !source_dir.is_dir() {
// Create source.tmp
let source_dir_tmp = recipe_dir.join("source.tmp");
@ -234,18 +235,6 @@ pub fn fetch(recipe: &CookRecipe, check_source: bool, logger: &PtyOut) -> Result
);
}
// Reset origin
let mut command = Command::new("git");
command.arg("-C").arg(&source_dir);
command.arg("remote").arg("set-url").arg("origin").arg(git);
run_command(command, logger)?;
// Fetch origin
let mut command = Command::new("git");
command.arg("-C").arg(&source_dir);
command.arg("fetch").arg("origin");
run_command(command, logger)?;
let (head_rev, detached_rev) = get_git_head_rev(&source_dir)?;
match (rev, detached_rev) {
(Some(rev), true) => {
@ -274,6 +263,8 @@ pub fn fetch(recipe: &CookRecipe, check_source: bool, logger: &PtyOut) -> Result
} else if remote_name != "origin" || &remote_url != chop_dot_git(git) {
false
} else {
git_run_fetch(logger, &source_dir, git)?;
fetch_is_ran = true;
match get_git_fetch_rev(&source_dir, &remote_url, &remote_branch) {
Ok(fetch_rev) => fetch_rev == head_rev,
Err(e) => {
@ -288,6 +279,9 @@ pub fn fetch(recipe: &CookRecipe, check_source: bool, logger: &PtyOut) -> Result
};
if !cached {
if !fetch_is_ran {
git_run_fetch(logger, &source_dir, git)?;
}
if let Some(_upstream) = upstream {
//TODO: set upstream URL (is this needed?)
// git remote set-url upstream "$GIT_UPSTREAM" &> /dev/null ||
@ -459,6 +453,18 @@ pub fn fetch(recipe: &CookRecipe, check_source: bool, logger: &PtyOut) -> Result
Ok(result)
}
fn git_run_fetch(logger: &PtyOut, source_dir: &PathBuf, git: &String) -> Result<()> {
let mut command = Command::new("git");
command.arg("-C").arg(source_dir);
command.arg("remote").arg("set-url").arg("origin").arg(git);
run_command(command, logger)?;
let mut command = Command::new("git");
command.arg("-C").arg(source_dir);
command.arg("fetch").arg("origin");
run_command(command, logger)?;
Ok(())
}
fn manual_git_recursive_submodule(
logger: &PtyOut,
source_dir: &PathBuf,

View File

@ -476,3 +476,35 @@ fn get_git_branch_name(local_branch_path: &str) -> Result<String> {
))?
.to_string())
}
pub fn get_git_commit_date(dir: &PathBuf) -> Result<String> {
let mut git = process::Command::new("git");
git.args(["log", "-1", "--date=iso-strict-local", "--format=%ad"]);
git.env("TZ", "UTC");
git.current_dir(dir);
git.stdout(Stdio::piped());
git.output()
.map_err(wrap_io_err!("Executing git log"))
.map(|s| String::from_utf8_lossy(&s.stdout).trim().to_string())
}
pub fn get_git_rev_before_date(dir: &PathBuf, date: &str) -> Result<String> {
let mut git = process::Command::new("git");
git.args(["rev-list", "-n", "1", &format!("--before={}", date), "HEAD"]);
git.current_dir(dir);
git.stdout(Stdio::piped());
let output = git
.output()
.map_err(wrap_io_err!("Executing git rev-list"))?;
let rev = String::from_utf8_lossy(&output.stdout).trim().to_string();
if rev.is_empty() {
return Err(Error::from(format!(
"No commit found before {} in {:?}",
date, dir
)));
}
Ok(rev)
}

View File

@ -187,6 +187,8 @@ pub struct CookRecipe {
/// If false, it's listed on install config
pub is_deps: bool,
pub rule: String,
/// whether if this recipe is pinned from cookbook.lock
pub pinned: bool,
}
impl Recipe {
@ -244,6 +246,7 @@ impl CookRecipe {
target,
is_deps: false,
rule: "".into(),
pinned: false,
})
}
@ -426,6 +429,10 @@ impl CookRecipe {
}
pub fn reload_recipe(&mut self) -> Result<(), PackageError> {
if self.pinned {
// TODO: print?
return Ok(());
}
self.recipe = Self::from_path(&self.dir, true, self.name.is_host())?.recipe;
let _ = self.apply_filesystem_config(&self.rule.clone());
Ok(())