Bump version to 0.18.1 and implement documentation splitting feature
- Updated version in Cargo.lock and Cargo.toml to 0.18.1. - Added functions to handle documentation package creation and management in `src/package/spec.rs`. - Implemented logic to split documentation into separate outputs during the staging process in `src/staging/mod.rs`. - Enhanced database interaction to retrieve package directories in `src/db/mod.rs`. - Refactored command handling in `src/commands.rs` to accommodate new output specifications. - Added tests to verify the correct behavior of documentation splitting and package management.
This commit is contained in:
Generated
+1
-1
@@ -418,7 +418,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "depot"
|
name = "depot"
|
||||||
version = "0.18.0"
|
version = "0.18.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"ar",
|
"ar",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "depot"
|
name = "depot"
|
||||||
version = "0.18.0"
|
version = "0.18.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[lints.rust]
|
[lints.rust]
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
project(
|
project(
|
||||||
'depot',
|
'depot',
|
||||||
version: '0.18.0',
|
version: '0.18.1',
|
||||||
meson_version: '>=0.60.0',
|
meson_version: '>=0.60.0',
|
||||||
default_options: ['buildtype=release'],
|
default_options: ['buildtype=release'],
|
||||||
)
|
)
|
||||||
|
|||||||
+79
-22
@@ -703,6 +703,66 @@ fn output_destdir_for(base_destdir: &Path, primary_pkg: &str, output_pkg: &str)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn spec_for_output(
|
||||||
|
pkg_spec: &package::PackageSpec,
|
||||||
|
output: package::PackageInfo,
|
||||||
|
) -> package::PackageSpec {
|
||||||
|
let output_name = output.name.clone();
|
||||||
|
let mut spec_for_out = pkg_spec.clone();
|
||||||
|
spec_for_out.package = output;
|
||||||
|
spec_for_out.alternatives = pkg_spec.alternatives_for_output(&output_name);
|
||||||
|
spec_for_out.dependencies = pkg_spec.dependencies_for_output(&output_name);
|
||||||
|
spec_for_out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destdir_has_packagable_content(destdir: &Path) -> Result<bool> {
|
||||||
|
if !destdir.exists() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let manifest = staging::generate_manifest_with_dirs(destdir)?;
|
||||||
|
Ok(!manifest.files.is_empty() || !manifest.directories.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn staged_output_specs(
|
||||||
|
pkg_spec: &package::PackageSpec,
|
||||||
|
destdir: &Path,
|
||||||
|
) -> Result<Vec<(package::PackageSpec, PathBuf)>> {
|
||||||
|
let declared_outputs = pkg_spec.outputs();
|
||||||
|
let declared_names: HashSet<String> = declared_outputs
|
||||||
|
.iter()
|
||||||
|
.map(|output| output.name.clone())
|
||||||
|
.collect();
|
||||||
|
let mut outputs = Vec::new();
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
|
||||||
|
for output in declared_outputs {
|
||||||
|
let output_name = output.name.clone();
|
||||||
|
let out_destdir = output_destdir_for(destdir, &pkg_spec.package.name, &output_name);
|
||||||
|
outputs.push((spec_for_output(pkg_spec, output.clone()), out_destdir));
|
||||||
|
seen.insert(output_name.clone());
|
||||||
|
|
||||||
|
if !pkg_spec.build.flags.split_docs || output_name.ends_with("-docs") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let docs_pkg = pkg_spec.docs_package_for_output(&output);
|
||||||
|
if declared_names.contains(&docs_pkg.name) || seen.contains(&docs_pkg.name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let docs_destdir = output_destdir_for(destdir, &pkg_spec.package.name, &docs_pkg.name);
|
||||||
|
if !destdir_has_packagable_content(&docs_destdir)? {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.insert(docs_pkg.name.clone());
|
||||||
|
outputs.push((spec_for_output(pkg_spec, docs_pkg), docs_destdir));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(outputs)
|
||||||
|
}
|
||||||
|
|
||||||
fn lib32_package_name(name: &str) -> String {
|
fn lib32_package_name(name: &str) -> String {
|
||||||
format!("lib32-{name}")
|
format!("lib32-{name}")
|
||||||
}
|
}
|
||||||
@@ -1187,15 +1247,15 @@ fn plan_staged_install(
|
|||||||
let db_path = config.installed_db_path(rootfs);
|
let db_path = config.installed_db_path(rootfs);
|
||||||
|
|
||||||
let is_update = db::get_package_version(&db_path, &pkg_spec.package.name)?.is_some();
|
let is_update = db::get_package_version(&db_path, &pkg_spec.package.name)?.is_some();
|
||||||
let new_files = staging::generate_manifest_with_dirs(destdir)?;
|
let new_manifest = staging::generate_manifest_with_dirs(destdir)?;
|
||||||
let remove_paths =
|
let remove_paths =
|
||||||
db::calculate_upgrade_paths(&db_path, &pkg_spec.package.name, &new_files.files)?;
|
db::calculate_upgrade_paths(&db_path, &pkg_spec.package.name, &new_manifest)?;
|
||||||
let operation = if is_update {
|
let operation = if is_update {
|
||||||
install::hooks::HookOperation::Update
|
install::hooks::HookOperation::Update
|
||||||
} else {
|
} else {
|
||||||
install::hooks::HookOperation::Install
|
install::hooks::HookOperation::Install
|
||||||
};
|
};
|
||||||
let mut affected_paths = new_files.files.clone();
|
let mut affected_paths = new_manifest.files.clone();
|
||||||
affected_paths.extend(remove_paths.iter().cloned());
|
affected_paths.extend(remove_paths.iter().cloned());
|
||||||
affected_paths.sort();
|
affected_paths.sort();
|
||||||
affected_paths.dedup();
|
affected_paths.dedup();
|
||||||
@@ -1218,13 +1278,7 @@ fn plan_package_outputs_for_install(
|
|||||||
config: &config::Config,
|
config: &config::Config,
|
||||||
) -> Result<Vec<PlannedPackageInstall>> {
|
) -> Result<Vec<PlannedPackageInstall>> {
|
||||||
let mut plans = Vec::new();
|
let mut plans = Vec::new();
|
||||||
for out in pkg_spec.outputs() {
|
for (spec_for_out, out_destdir) in staged_output_specs(pkg_spec, destdir)? {
|
||||||
let mut spec_for_out = pkg_spec.clone();
|
|
||||||
let output_name = out.name.clone();
|
|
||||||
spec_for_out.package = out;
|
|
||||||
spec_for_out.alternatives = pkg_spec.alternatives_for_output(&output_name);
|
|
||||||
spec_for_out.dependencies = pkg_spec.dependencies_for_output(&output_name);
|
|
||||||
let out_destdir = output_destdir_for(destdir, &pkg_spec.package.name, &output_name);
|
|
||||||
let staged = plan_staged_install(&spec_for_out, &out_destdir, rootfs, config)?;
|
let staged = plan_staged_install(&spec_for_out, &out_destdir, rootfs, config)?;
|
||||||
plans.push(PlannedPackageInstall {
|
plans.push(PlannedPackageInstall {
|
||||||
spec: spec_for_out,
|
spec: spec_for_out,
|
||||||
@@ -4077,17 +4131,18 @@ pub fn run(cli: Cli) -> Result<()> {
|
|||||||
.unwrap_or(std::env::consts::ARCH);
|
.unwrap_or(std::env::consts::ARCH);
|
||||||
|
|
||||||
let mut created_files = Vec::new();
|
let mut created_files = Vec::new();
|
||||||
|
let staged_outputs = if !cli.lib32_only {
|
||||||
|
staged_output_specs(&pkg_spec, &destdir)?
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
if !cli.lib32_only {
|
if !cli.lib32_only {
|
||||||
for out in pkg_spec.outputs() {
|
for (spec_for_out, out_destdir) in &staged_outputs {
|
||||||
let mut spec_for_out = pkg_spec.clone();
|
let packager = package::Packager::new(
|
||||||
let output_name = out.name.clone();
|
spec_for_out.clone(),
|
||||||
spec_for_out.package = out;
|
out_destdir.clone(),
|
||||||
spec_for_out.alternatives = pkg_spec.alternatives_for_output(&output_name);
|
config.clone(),
|
||||||
spec_for_out.dependencies = pkg_spec.dependencies_for_output(&output_name);
|
);
|
||||||
let out_destdir =
|
|
||||||
output_destdir_for(&destdir, &pkg_spec.package.name, &output_name);
|
|
||||||
let packager =
|
|
||||||
package::Packager::new(spec_for_out.clone(), out_destdir, config.clone());
|
|
||||||
let pkg_file = packager.create_package(Path::new("."), arch)?;
|
let pkg_file = packager.create_package(Path::new("."), arch)?;
|
||||||
if let Some(sig_path) =
|
if let Some(sig_path) =
|
||||||
signing::auto_sign_zst_file_detached(&cli.rootfs, &pkg_file)?
|
signing::auto_sign_zst_file_detached(&cli.rootfs, &pkg_file)?
|
||||||
@@ -4137,7 +4192,8 @@ pub fn run(cli: Cli) -> Result<()> {
|
|||||||
if install {
|
if install {
|
||||||
let mut install_targets = Vec::new();
|
let mut install_targets = Vec::new();
|
||||||
if !cli.lib32_only {
|
if !cli.lib32_only {
|
||||||
for out in pkg_spec.outputs() {
|
for (spec_for_out, _) in &staged_outputs {
|
||||||
|
let out = &spec_for_out.package;
|
||||||
install_targets
|
install_targets
|
||||||
.push(format!("{} v{}-{}", out.name, out.version, out.revision));
|
.push(format!("{} v{}-{}", out.name, out.version, out.revision));
|
||||||
}
|
}
|
||||||
@@ -4213,7 +4269,8 @@ pub fn run(cli: Cli) -> Result<()> {
|
|||||||
install::scripts::run_deferred_hooks_if_possible(&cli.rootfs)?;
|
install::scripts::run_deferred_hooks_if_possible(&cli.rootfs)?;
|
||||||
} else {
|
} else {
|
||||||
if !cli.lib32_only {
|
if !cli.lib32_only {
|
||||||
for out in pkg_spec.outputs() {
|
for (spec_for_out, _) in &staged_outputs {
|
||||||
|
let out = &spec_for_out.package;
|
||||||
ui::success(format!(
|
ui::success(format!(
|
||||||
"Built successfully: {}-{}-{}",
|
"Built successfully: {}-{}-{}",
|
||||||
out.name, out.version, out.revision
|
out.name, out.version, out.revision
|
||||||
|
|||||||
+53
-11
@@ -199,6 +199,36 @@ pub fn get_package_files(db_path: &Path, name: &str) -> Result<Vec<String>> {
|
|||||||
Ok(files)
|
Ok(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the list of directories owned by an installed package.
|
||||||
|
pub fn get_package_directories(db_path: &Path, name: &str) -> Result<Vec<String>> {
|
||||||
|
if !db_path.exists() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let conn = Connection::open(db_path)?;
|
||||||
|
init_db(&conn)?;
|
||||||
|
|
||||||
|
let pkg_id_res: rusqlite::Result<i64> = conn.query_row(
|
||||||
|
"SELECT id FROM packages WHERE name = ?1",
|
||||||
|
params![name],
|
||||||
|
|row| row.get(0),
|
||||||
|
);
|
||||||
|
|
||||||
|
let pkg_id = match pkg_id_res {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(Vec::new()),
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut stmt = conn.prepare("SELECT path FROM directories WHERE package_id = ?1")?;
|
||||||
|
let mut directories: Vec<String> = stmt
|
||||||
|
.query_map(params![pkg_id], |row| row.get(0))?
|
||||||
|
.filter_map(|r| r.ok())
|
||||||
|
.collect();
|
||||||
|
directories.sort_by_key(|path| std::cmp::Reverse(path.matches('/').count()));
|
||||||
|
Ok(directories)
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove a package from the database and filesystem
|
/// Remove a package from the database and filesystem
|
||||||
pub fn remove_package(db_path: &Path, name: &str, rootfs: &Path) -> Result<()> {
|
pub fn remove_package(db_path: &Path, name: &str, rootfs: &Path) -> Result<()> {
|
||||||
if !db_path.exists() {
|
if !db_path.exists() {
|
||||||
@@ -510,18 +540,24 @@ fn detect_rootfs_from_db_path(db_path: &Path) -> Option<std::path::PathBuf> {
|
|||||||
pub fn calculate_upgrade_paths(
|
pub fn calculate_upgrade_paths(
|
||||||
db_path: &Path,
|
db_path: &Path,
|
||||||
name: &str,
|
name: &str,
|
||||||
new_files: &[String],
|
new_manifest: &staging::Manifest,
|
||||||
) -> Result<Vec<String>> {
|
) -> Result<Vec<String>> {
|
||||||
let old_files = get_package_files(db_path, name)?;
|
let old_files = get_package_files(db_path, name)?;
|
||||||
let mut new_set = std::collections::HashSet::new();
|
let old_directories = get_package_directories(db_path, name)?;
|
||||||
for f in new_files {
|
let new_files: std::collections::HashSet<_> = new_manifest.files.iter().cloned().collect();
|
||||||
new_set.insert(f);
|
let new_directories: std::collections::HashSet<_> =
|
||||||
}
|
new_manifest.directories.iter().cloned().collect();
|
||||||
|
|
||||||
let remove_paths: Vec<String> = old_files
|
let mut remove_paths: Vec<String> = old_files
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|p| !new_set.contains(p))
|
.filter(|p| !new_files.contains(p))
|
||||||
.collect();
|
.collect();
|
||||||
|
remove_paths.extend(
|
||||||
|
old_directories
|
||||||
|
.into_iter()
|
||||||
|
.filter(|p| !new_directories.contains(p)),
|
||||||
|
);
|
||||||
|
remove_paths.sort_by_key(|path| std::cmp::Reverse(path.matches('/').count()));
|
||||||
|
|
||||||
Ok(remove_paths)
|
Ok(remove_paths)
|
||||||
}
|
}
|
||||||
@@ -871,9 +907,12 @@ mod tests {
|
|||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let db_path = tmp.path().join("packages.db");
|
let db_path = tmp.path().join("packages.db");
|
||||||
std::fs::File::create(&db_path).unwrap();
|
std::fs::File::create(&db_path).unwrap();
|
||||||
|
let manifest = staging::Manifest {
|
||||||
|
files: vec!["usr/bin/foo".to_string()],
|
||||||
|
directories: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
let remove_paths =
|
let remove_paths = calculate_upgrade_paths(&db_path, "nonexistent", &manifest).unwrap();
|
||||||
calculate_upgrade_paths(&db_path, "nonexistent", &["usr/bin/foo".to_string()]).unwrap();
|
|
||||||
assert!(remove_paths.is_empty());
|
assert!(remove_paths.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -943,11 +982,14 @@ mod tests {
|
|||||||
std::fs::write(dest2.join("usr/bin/new_file"), "new").unwrap();
|
std::fs::write(dest2.join("usr/bin/new_file"), "new").unwrap();
|
||||||
|
|
||||||
let manifest2 = crate::staging::generate_manifest_with_dirs(&dest2).unwrap();
|
let manifest2 = crate::staging::generate_manifest_with_dirs(&dest2).unwrap();
|
||||||
let remove_paths = calculate_upgrade_paths(&db_path, "foo", &manifest2.files).unwrap();
|
let remove_paths = calculate_upgrade_paths(&db_path, "foo", &manifest2).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
remove_paths,
|
remove_paths,
|
||||||
vec!["usr/bin/shared_dir/old_file".to_string()]
|
vec![
|
||||||
|
"usr/bin/shared_dir/old_file".to_string(),
|
||||||
|
"usr/bin/shared_dir".to_string()
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
let tx =
|
let tx =
|
||||||
|
|||||||
+159
-1
@@ -188,7 +188,7 @@ impl PackageSpec {
|
|||||||
matches!(self.build.build_type, BuildType::Meta)
|
matches!(self.build.build_type, BuildType::Meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return all package outputs this spec will produce (primary + any extras)
|
/// Return all declared package outputs for this spec (primary + any extras).
|
||||||
pub fn outputs(&self) -> Vec<PackageInfo> {
|
pub fn outputs(&self) -> Vec<PackageInfo> {
|
||||||
let mut v = Vec::new();
|
let mut v = Vec::new();
|
||||||
v.push(self.package.clone());
|
v.push(self.package.clone());
|
||||||
@@ -196,10 +196,47 @@ impl PackageSpec {
|
|||||||
v
|
v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the derived documentation package name for an output package.
|
||||||
|
pub fn docs_package_name(pkg_name: &str) -> String {
|
||||||
|
format!("{pkg_name}-docs")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build package metadata for an automatically generated documentation output.
|
||||||
|
pub fn docs_package_for_output(&self, output: &PackageInfo) -> PackageInfo {
|
||||||
|
let mut docs = output.clone();
|
||||||
|
docs.name = Self::docs_package_name(&output.name);
|
||||||
|
docs.description = format!("Documentation for {}", output.name);
|
||||||
|
docs
|
||||||
|
}
|
||||||
|
|
||||||
|
fn docs_parent_output_name(&self, pkg_name: &str) -> Option<String> {
|
||||||
|
if !self.build.flags.split_docs {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let base = pkg_name.strip_suffix("-docs")?;
|
||||||
|
self.outputs()
|
||||||
|
.into_iter()
|
||||||
|
.find(|output| output.name == base)
|
||||||
|
.map(|output| output.name)
|
||||||
|
}
|
||||||
|
|
||||||
/// Return dependencies for a specific output package name.
|
/// Return dependencies for a specific output package name.
|
||||||
///
|
///
|
||||||
/// If no per-output override exists, returns the top-level dependencies.
|
/// If no per-output override exists, returns the top-level dependencies.
|
||||||
pub fn dependencies_for_output(&self, pkg_name: &str) -> Dependencies {
|
pub fn dependencies_for_output(&self, pkg_name: &str) -> Dependencies {
|
||||||
|
if let Some(parent_output) = self.docs_parent_output_name(pkg_name) {
|
||||||
|
return self
|
||||||
|
.package_dependencies
|
||||||
|
.get(pkg_name)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
let mut deps = Dependencies::default();
|
||||||
|
deps.runtime.push(parent_output);
|
||||||
|
deps
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
self.package_dependencies
|
self.package_dependencies
|
||||||
.get(pkg_name)
|
.get(pkg_name)
|
||||||
.cloned()
|
.cloned()
|
||||||
@@ -226,6 +263,14 @@ impl PackageSpec {
|
|||||||
///
|
///
|
||||||
/// If no per-output override exists, returns the top-level alternatives.
|
/// If no per-output override exists, returns the top-level alternatives.
|
||||||
pub fn alternatives_for_output(&self, pkg_name: &str) -> Alternatives {
|
pub fn alternatives_for_output(&self, pkg_name: &str) -> Alternatives {
|
||||||
|
if self.docs_parent_output_name(pkg_name).is_some() {
|
||||||
|
return self
|
||||||
|
.package_alternatives
|
||||||
|
.get(pkg_name)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
}
|
||||||
|
|
||||||
self.package_alternatives
|
self.package_alternatives
|
||||||
.get(pkg_name)
|
.get(pkg_name)
|
||||||
.cloned()
|
.cloned()
|
||||||
@@ -424,6 +469,22 @@ impl PackageSpec {
|
|||||||
self.build.flags.keep = vec![s.to_string()];
|
self.build.flags.keep = vec![s.to_string()];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"split_docs" | "split-docs" => {
|
||||||
|
if let Some(b) = toml_value_as_boolish(v) {
|
||||||
|
self.build.flags.split_docs = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"doc_dirs" | "doc-dirs" => {
|
||||||
|
if let Some(arr) = v.as_array() {
|
||||||
|
self.build.flags.doc_dirs = arr
|
||||||
|
.iter()
|
||||||
|
.filter_map(|x| x.as_str())
|
||||||
|
.map(String::from)
|
||||||
|
.collect();
|
||||||
|
} else if let Some(s) = v.as_str() {
|
||||||
|
self.build.flags.doc_dirs = vec![s.to_string()];
|
||||||
|
}
|
||||||
|
}
|
||||||
"cc" => {
|
"cc" => {
|
||||||
if let Some(s) = v.as_str() {
|
if let Some(s) = v.as_str() {
|
||||||
self.build.flags.cc = s.to_string();
|
self.build.flags.cc = s.to_string();
|
||||||
@@ -978,6 +1039,18 @@ impl PackageSpec {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"doc_dirs" | "doc-dirs" => {
|
||||||
|
for v in values {
|
||||||
|
if let Some(arr) = v.as_array() {
|
||||||
|
self.build
|
||||||
|
.flags
|
||||||
|
.doc_dirs
|
||||||
|
.extend(arr.iter().filter_map(|x| x.as_str()).map(String::from));
|
||||||
|
} else if let Some(s) = v.as_str() {
|
||||||
|
self.build.flags.doc_dirs.push(s.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
"configure" => {
|
"configure" => {
|
||||||
for v in values {
|
for v in values {
|
||||||
if let Some(arr) = v.as_array() {
|
if let Some(arr) = v.as_array() {
|
||||||
@@ -1452,6 +1525,11 @@ impl PackageSpec {
|
|||||||
self.build.flags.build_32 = b;
|
self.build.flags.build_32 = b;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"split_docs" | "split-docs" => {
|
||||||
|
if let Some(b) = values.last().and_then(toml_value_as_boolish) {
|
||||||
|
self.build.flags.split_docs = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2936,6 +3014,44 @@ keep = ["etc/locale.gen", "etc/resolv.conf"]
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_split_docs_from_spec() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let path = tmp.path().join("pkg.toml");
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
&path,
|
||||||
|
r#"
|
||||||
|
[package]
|
||||||
|
name = "foo"
|
||||||
|
version = "1.0"
|
||||||
|
description = "d"
|
||||||
|
homepage = "h"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[source]
|
||||||
|
url = "https://example.com/foo.tar.gz"
|
||||||
|
sha256 = "skip"
|
||||||
|
extract_dir = "foo"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
type = "custom"
|
||||||
|
|
||||||
|
[build.flags]
|
||||||
|
split_docs = true
|
||||||
|
doc_dirs = ["/opt/docs", "usr/share/devhelp"]
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let spec = PackageSpec::from_file(&path).unwrap();
|
||||||
|
assert!(spec.build.flags.split_docs);
|
||||||
|
assert_eq!(
|
||||||
|
spec.build.flags.doc_dirs,
|
||||||
|
vec!["/opt/docs".to_string(), "usr/share/devhelp".to_string()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_build_flags_appends_from_spec_file() {
|
fn parse_build_flags_appends_from_spec_file() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
@@ -3289,6 +3405,31 @@ type = "custom"
|
|||||||
assert_eq!(outputs[1].name, "foo-dev");
|
assert_eq!(outputs[1].name, "foo-dev");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn docs_output_uses_runtime_dependency_on_parent_package() {
|
||||||
|
let mut spec = mk_spec("foo", "1.0");
|
||||||
|
spec.build.flags.split_docs = true;
|
||||||
|
let docs_name = PackageSpec::docs_package_name("foo");
|
||||||
|
|
||||||
|
let deps = spec.dependencies_for_output(&docs_name);
|
||||||
|
assert_eq!(deps.runtime, vec!["foo".to_string()]);
|
||||||
|
|
||||||
|
let alternatives = spec.alternatives_for_output(&docs_name);
|
||||||
|
assert!(alternatives.provides.is_empty());
|
||||||
|
assert!(alternatives.conflicts.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn docs_package_for_output_derives_name_and_description() {
|
||||||
|
let mut spec = mk_spec("foo", "1.0");
|
||||||
|
spec.build.flags.split_docs = true;
|
||||||
|
|
||||||
|
let docs = spec.docs_package_for_output(&spec.package);
|
||||||
|
assert_eq!(docs.name, "foo-docs");
|
||||||
|
assert_eq!(docs.description, "Documentation for foo");
|
||||||
|
assert_eq!(docs.version, "1.0");
|
||||||
|
}
|
||||||
|
|
||||||
fn mk_spec(name: &str, version: &str) -> PackageSpec {
|
fn mk_spec(name: &str, version: &str) -> PackageSpec {
|
||||||
PackageSpec {
|
PackageSpec {
|
||||||
package: PackageInfo {
|
package: PackageInfo {
|
||||||
@@ -3625,6 +3766,21 @@ pub struct BuildFlags {
|
|||||||
/// Keep existing files and install package-provided replacement as `<path>.depotnew`.
|
/// Keep existing files and install package-provided replacement as `<path>.depotnew`.
|
||||||
#[serde(default, deserialize_with = "deserialize_string_or_array")]
|
#[serde(default, deserialize_with = "deserialize_string_or_array")]
|
||||||
pub keep: Vec<String>,
|
pub keep: Vec<String>,
|
||||||
|
/// Split documentation trees into a derived `<package>-docs` output during staging.
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
alias = "split-docs",
|
||||||
|
deserialize_with = "deserialize_boolish"
|
||||||
|
)]
|
||||||
|
pub split_docs: bool,
|
||||||
|
/// Additional documentation directories to move into `<package>-docs`.
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
alias = "doc-dirs",
|
||||||
|
alias = "doc_dirs",
|
||||||
|
deserialize_with = "deserialize_string_or_array_no_split"
|
||||||
|
)]
|
||||||
|
pub doc_dirs: Vec<String>,
|
||||||
/// Disable automatic LTOFLAGS injection into CFLAGS/CXXFLAGS/LDFLAGS.
|
/// Disable automatic LTOFLAGS injection into CFLAGS/CXXFLAGS/LDFLAGS.
|
||||||
#[serde(
|
#[serde(
|
||||||
default = "default_use_lto",
|
default = "default_use_lto",
|
||||||
@@ -3960,6 +4116,8 @@ impl Default for BuildFlags {
|
|||||||
ltoflags: Vec::new(),
|
ltoflags: Vec::new(),
|
||||||
replace_ltoflags: Vec::new(),
|
replace_ltoflags: Vec::new(),
|
||||||
keep: Vec::new(),
|
keep: Vec::new(),
|
||||||
|
split_docs: false,
|
||||||
|
doc_dirs: Vec::new(),
|
||||||
use_lto: default_use_lto(),
|
use_lto: default_use_lto(),
|
||||||
no_flags: false,
|
no_flags: false,
|
||||||
no_strip: false,
|
no_strip: false,
|
||||||
|
|||||||
+498
-73
@@ -208,6 +208,202 @@ fn is_manpage_rel_path(rel_path: &str) -> bool {
|
|||||||
rel.starts_with("usr/share/man/") && !has_known_compressed_suffix(rel)
|
rel.starts_with("usr/share/man/") && !has_known_compressed_suffix(rel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalize_doc_dir(path: &str) -> Result<String> {
|
||||||
|
let trimmed = path.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
anyhow::bail!("doc_dirs entries must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
let relative = trimmed.trim_start_matches('/');
|
||||||
|
if relative.is_empty() {
|
||||||
|
anyhow::bail!("doc_dirs entries must not resolve to the filesystem root");
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = Path::new(relative);
|
||||||
|
let mut normalized = PathBuf::new();
|
||||||
|
for comp in p.components() {
|
||||||
|
match comp {
|
||||||
|
Component::Normal(seg) => normalized.push(seg),
|
||||||
|
Component::CurDir => {}
|
||||||
|
_ => {
|
||||||
|
anyhow::bail!(
|
||||||
|
"doc_dirs entries must not contain traversal or root components: {}",
|
||||||
|
trimmed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalized = normalized
|
||||||
|
.to_str()
|
||||||
|
.context("doc_dirs entries must be valid UTF-8")?
|
||||||
|
.to_string();
|
||||||
|
if normalized.is_empty() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"doc_dirs entries must not resolve to an empty path: {}",
|
||||||
|
trimmed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup_empty_parent_dirs(root: &Path, start: &Path) -> Result<()> {
|
||||||
|
let mut current = start.parent();
|
||||||
|
while let Some(dir) = current {
|
||||||
|
if dir == root {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
match fs::remove_dir(dir) {
|
||||||
|
Ok(()) => current = dir.parent(),
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::DirectoryNotEmpty => break,
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::NotFound => current = dir.parent(),
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err)
|
||||||
|
.with_context(|| format!("Failed to prune empty dir {}", dir.display()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_tree_preserving_layout(src: &Path, dst: &Path) -> Result<()> {
|
||||||
|
let metadata = src
|
||||||
|
.symlink_metadata()
|
||||||
|
.with_context(|| format!("Failed to inspect {}", src.display()))?;
|
||||||
|
let file_type = metadata.file_type();
|
||||||
|
|
||||||
|
if file_type.is_dir() {
|
||||||
|
match dst.symlink_metadata() {
|
||||||
|
Ok(dst_meta) => {
|
||||||
|
if !dst_meta.file_type().is_dir() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Failed to move {} into {}: destination exists and is not a directory",
|
||||||
|
src.display(),
|
||||||
|
dst.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for entry in fs::read_dir(src)
|
||||||
|
.with_context(|| format!("Failed to read {}", src.display()))?
|
||||||
|
{
|
||||||
|
let entry = entry?;
|
||||||
|
let child_src = entry.path();
|
||||||
|
let child_dst = dst.join(entry.file_name());
|
||||||
|
move_tree_preserving_layout(&child_src, &child_dst)?;
|
||||||
|
}
|
||||||
|
fs::remove_dir(src)
|
||||||
|
.with_context(|| format!("Failed to remove {}", src.display()))?;
|
||||||
|
}
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
||||||
|
if let Some(parent) = dst.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.with_context(|| format!("Failed to create {}", parent.display()))?;
|
||||||
|
}
|
||||||
|
fs::rename(src, dst).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to move documentation tree {} -> {}",
|
||||||
|
src.display(),
|
||||||
|
dst.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err).with_context(|| format!("Failed to inspect {}", dst.display()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Some(parent) = dst.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.with_context(|| format!("Failed to create {}", parent.display()))?;
|
||||||
|
}
|
||||||
|
if dst.symlink_metadata().is_ok() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Failed to move {} into {}: destination already exists",
|
||||||
|
src.display(),
|
||||||
|
dst.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
fs::rename(src, dst).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to move documentation path {} -> {}",
|
||||||
|
src.display(),
|
||||||
|
dst.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_docs_for_output(
|
||||||
|
output_destdir: &Path,
|
||||||
|
docs_destdir: &Path,
|
||||||
|
doc_dirs: &[String],
|
||||||
|
) -> Result<usize> {
|
||||||
|
let mut moved = 0usize;
|
||||||
|
|
||||||
|
for rel_dir in doc_dirs {
|
||||||
|
let src = output_destdir.join(rel_dir);
|
||||||
|
let metadata = match src.symlink_metadata() {
|
||||||
|
Ok(metadata) => metadata,
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err).with_context(|| format!("Failed to inspect {}", src.display()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !metadata.file_type().is_dir() && !metadata.file_type().is_symlink() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dst = docs_destdir.join(rel_dir);
|
||||||
|
move_tree_preserving_layout(&src, &dst)?;
|
||||||
|
cleanup_empty_parent_dirs(output_destdir, &src)?;
|
||||||
|
moved += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(moved)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_docs_outputs(destdir: &Path, spec: &PackageSpec) -> Result<usize> {
|
||||||
|
if !spec.build.flags.split_docs {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut doc_dirs = vec![
|
||||||
|
normalize_doc_dir("/usr/share/doc")?,
|
||||||
|
normalize_doc_dir("/usr/share/gtk-doc")?,
|
||||||
|
];
|
||||||
|
for custom in &spec.build.flags.doc_dirs {
|
||||||
|
let normalized = normalize_doc_dir(custom)?;
|
||||||
|
if !doc_dirs.contains(&normalized) {
|
||||||
|
doc_dirs.push(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut moved = 0usize;
|
||||||
|
for output in spec.outputs() {
|
||||||
|
if output.name.ends_with("-docs") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output_destdir = if output.name == spec.package.name {
|
||||||
|
destdir.to_path_buf()
|
||||||
|
} else {
|
||||||
|
output_staging_dir(destdir, &output.name)
|
||||||
|
};
|
||||||
|
if !output_destdir.exists() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let docs_pkg = spec.docs_package_for_output(&output);
|
||||||
|
let docs_destdir = output_staging_dir(destdir, &docs_pkg.name);
|
||||||
|
moved += split_docs_for_output(&output_destdir, &docs_destdir, &doc_dirs)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(moved)
|
||||||
|
}
|
||||||
|
|
||||||
fn append_os_suffix(path: &Path, suffix: &str) -> PathBuf {
|
fn append_os_suffix(path: &Path, suffix: &str) -> PathBuf {
|
||||||
let mut s = OsString::from(path.as_os_str());
|
let mut s = OsString::from(path.as_os_str());
|
||||||
s.push(suffix);
|
s.push(suffix);
|
||||||
@@ -460,6 +656,14 @@ pub fn process(destdir: &Path, spec: &PackageSpec) -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let moved_docs = split_docs_outputs(destdir, spec)?;
|
||||||
|
if moved_docs > 0 {
|
||||||
|
crate::log_info!(
|
||||||
|
"Moved {} documentation tree(s) into docs outputs",
|
||||||
|
moved_docs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,6 +732,111 @@ pub struct FsTransaction {
|
|||||||
removed: Vec<String>,
|
removed: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_directory_empty(path: &Path) -> Result<bool> {
|
||||||
|
let mut entries = fs::read_dir(path)
|
||||||
|
.with_context(|| format!("Failed to read directory {}", path.display()))?;
|
||||||
|
Ok(entries.next().transpose()?.is_none())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backup_existing_path(src: &Path, backup_path: &Path, rel: &str) -> Result<()> {
|
||||||
|
let metadata = src
|
||||||
|
.symlink_metadata()
|
||||||
|
.with_context(|| format!("Failed to inspect existing path {}", rel))?;
|
||||||
|
|
||||||
|
if let Some(parent) = backup_path.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.with_context(|| format!("Failed to create backup dir {}", parent.display()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if metadata.file_type().is_symlink() {
|
||||||
|
let target = fs::read_link(src)
|
||||||
|
.with_context(|| format!("Failed to read existing symlink target {}", rel))?;
|
||||||
|
std::os::unix::fs::symlink(&target, backup_path)
|
||||||
|
.with_context(|| format!("Failed to backup symlink {}", rel))?;
|
||||||
|
} else if metadata.file_type().is_dir() {
|
||||||
|
fs::create_dir_all(backup_path)
|
||||||
|
.with_context(|| format!("Failed to backup directory {}", rel))?;
|
||||||
|
apply_unix_mode(backup_path, &metadata)?;
|
||||||
|
} else {
|
||||||
|
fs::copy(src, backup_path).with_context(|| format!("Failed to backup file {}", rel))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_path_in_place(path: &Path, rel: &str) -> Result<()> {
|
||||||
|
let metadata = path
|
||||||
|
.symlink_metadata()
|
||||||
|
.with_context(|| format!("Failed to inspect existing path {}", rel))?;
|
||||||
|
|
||||||
|
if metadata.file_type().is_dir() {
|
||||||
|
fs::remove_dir(path)
|
||||||
|
.with_context(|| format!("Failed to remove obsolete directory {}", rel))?;
|
||||||
|
} else {
|
||||||
|
fs::remove_file(path)
|
||||||
|
.with_context(|| format!("Failed to remove obsolete file/symlink {}", rel))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backup_and_remove_obsolete_path(
|
||||||
|
tx: &mut FsTransaction,
|
||||||
|
rootfs: &Path,
|
||||||
|
rel: &str,
|
||||||
|
require_empty_dir: bool,
|
||||||
|
) -> Result<bool> {
|
||||||
|
let dest_path = rootfs.join(rel);
|
||||||
|
let metadata = match dest_path.symlink_metadata() {
|
||||||
|
Ok(metadata) => metadata,
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(false),
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err).with_context(|| {
|
||||||
|
format!("Failed to inspect obsolete path before removal: {}", rel)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if metadata.file_type().is_dir() {
|
||||||
|
let empty = is_directory_empty(&dest_path)?;
|
||||||
|
if !empty {
|
||||||
|
if require_empty_dir {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Refusing to replace existing non-empty directory with packaged file/symlink: {}",
|
||||||
|
rel
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let backup_path = tx.removed_backup_path(rel);
|
||||||
|
backup_existing_path(&dest_path, &backup_path, rel)?;
|
||||||
|
remove_path_in_place(&dest_path, rel)?;
|
||||||
|
tx.removed.push(rel.to_string());
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_obsolete_children_for_dir(
|
||||||
|
tx: &mut FsTransaction,
|
||||||
|
rootfs: &Path,
|
||||||
|
dir_rel: &str,
|
||||||
|
remove_paths: &[String],
|
||||||
|
) -> Result<()> {
|
||||||
|
let prefix = format!("{dir_rel}/");
|
||||||
|
let mut nested_paths: Vec<&str> = remove_paths
|
||||||
|
.iter()
|
||||||
|
.filter_map(|path| path.strip_prefix(&prefix).map(|_| path.as_str()))
|
||||||
|
.collect();
|
||||||
|
nested_paths.sort_by_key(|path| std::cmp::Reverse(path.matches('/').count()));
|
||||||
|
|
||||||
|
for rel in nested_paths {
|
||||||
|
let _ = backup_and_remove_obsolete_path(tx, rootfs, rel, false)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
impl FsTransaction {
|
impl FsTransaction {
|
||||||
fn backup_path(&self, rel: &str) -> PathBuf {
|
fn backup_path(&self, rel: &str) -> PathBuf {
|
||||||
self.tx_dir.join("backup").join(rel)
|
self.tx_dir.join("backup").join(rel)
|
||||||
@@ -537,44 +846,78 @@ impl FsTransaction {
|
|||||||
self.tx_dir.join("removed").join(rel)
|
self.tx_dir.join("removed").join(rel)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Roll back file operations performed by `install_atomic`.
|
fn restore_backup_entry(&self, src: &Path, dst: &Path) -> Result<()> {
|
||||||
pub fn rollback(&self) -> Result<()> {
|
let metadata = src
|
||||||
// Restore removed files
|
.symlink_metadata()
|
||||||
for rel in &self.removed {
|
.with_context(|| format!("Failed to inspect backup entry {}", src.display()))?;
|
||||||
let src = self.removed_backup_path(rel);
|
|
||||||
let dst = self.rootfs.join(rel);
|
|
||||||
if src.symlink_metadata().is_ok() {
|
|
||||||
if let Some(parent) = dst.parent() {
|
if let Some(parent) = dst.parent() {
|
||||||
fs::create_dir_all(parent)?;
|
fs::create_dir_all(parent)
|
||||||
|
.with_context(|| format!("Failed to create restore dir {}", parent.display()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if metadata.file_type().is_dir() {
|
||||||
|
match dst.symlink_metadata() {
|
||||||
|
Ok(dst_meta) if dst_meta.file_type().is_dir() => {}
|
||||||
|
Ok(dst_meta) if dst_meta.file_type().is_symlink() => {
|
||||||
|
fs::remove_file(dst)
|
||||||
|
.with_context(|| format!("Failed to remove {}", dst.display()))?;
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
fs::remove_file(dst)
|
||||||
|
.with_context(|| format!("Failed to remove {}", dst.display()))?;
|
||||||
|
}
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err)
|
||||||
|
.with_context(|| format!("Failed to inspect {}", dst.display()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs::create_dir_all(dst)
|
||||||
|
.with_context(|| format!("Failed to restore directory {}", dst.display()))?;
|
||||||
|
apply_unix_mode(dst, &metadata)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = fs::remove_file(dst);
|
||||||
|
match fs::rename(src, dst) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(_) if metadata.file_type().is_symlink() => {
|
||||||
|
let target = fs::read_link(src)
|
||||||
|
.with_context(|| format!("Failed to read backup symlink {}", src.display()))?;
|
||||||
|
std::os::unix::fs::symlink(&target, dst)
|
||||||
|
.with_context(|| format!("Failed to restore symlink {}", dst.display()))
|
||||||
}
|
}
|
||||||
// Best-effort remove if something exists now.
|
|
||||||
let _ = fs::remove_file(&dst);
|
|
||||||
match fs::rename(&src, &dst) {
|
|
||||||
Ok(()) => {}
|
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
fs::copy(&src, &dst)?;
|
fs::copy(src, dst).with_context(|| {
|
||||||
fs::remove_file(&src)?;
|
format!(
|
||||||
}
|
"Failed to restore file {} from {}",
|
||||||
|
dst.display(),
|
||||||
|
src.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore overwritten files
|
/// Roll back file operations performed by `install_atomic`.
|
||||||
|
pub fn rollback(&self) -> Result<()> {
|
||||||
|
// Restore overwritten paths first so removed children have their original parent layout.
|
||||||
for rel in &self.backed_up {
|
for rel in &self.backed_up {
|
||||||
let src = self.backup_path(rel);
|
let src = self.backup_path(rel);
|
||||||
let dst = self.rootfs.join(rel);
|
let dst = self.rootfs.join(rel);
|
||||||
if src.symlink_metadata().is_ok() {
|
if src.symlink_metadata().is_ok() {
|
||||||
if let Some(parent) = dst.parent() {
|
self.restore_backup_entry(&src, &dst)?;
|
||||||
fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
let _ = fs::remove_file(&dst);
|
|
||||||
match fs::rename(&src, &dst) {
|
|
||||||
Ok(()) => {}
|
|
||||||
Err(_) => {
|
|
||||||
fs::copy(&src, &dst)?;
|
|
||||||
fs::remove_file(&src)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore removed files/directories.
|
||||||
|
for rel in &self.removed {
|
||||||
|
let src = self.removed_backup_path(rel);
|
||||||
|
let dst = self.rootfs.join(rel);
|
||||||
|
if src.symlink_metadata().is_ok() {
|
||||||
|
self.restore_backup_entry(&src, &dst)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,6 +961,7 @@ pub fn install_atomic(
|
|||||||
KeepMatcher::Pattern(_) => None,
|
KeepMatcher::Pattern(_) => None,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
let remove_set: HashSet<&str> = remove_paths.iter().map(String::as_str).collect();
|
||||||
|
|
||||||
fs::create_dir_all(tx_base_dir)
|
fs::create_dir_all(tx_base_dir)
|
||||||
.with_context(|| format!("Failed to create tx dir: {}", tx_base_dir.display()))?;
|
.with_context(|| format!("Failed to create tx dir: {}", tx_base_dir.display()))?;
|
||||||
@@ -640,6 +984,7 @@ pub fn install_atomic(
|
|||||||
created: Vec::new(),
|
created: Vec::new(),
|
||||||
removed: Vec::new(),
|
removed: Vec::new(),
|
||||||
};
|
};
|
||||||
|
let mut staged_paths = HashSet::new();
|
||||||
|
|
||||||
let result: Result<()> = (|| {
|
let result: Result<()> = (|| {
|
||||||
// First, create all directories from destdir (for packages with only directories)
|
// First, create all directories from destdir (for packages with only directories)
|
||||||
@@ -664,6 +1009,7 @@ pub fn install_atomic(
|
|||||||
fs::create_dir_all(&dest_path)?;
|
fs::create_dir_all(&dest_path)?;
|
||||||
apply_unix_mode(&dest_path, &src_path.symlink_metadata()?)?;
|
apply_unix_mode(&dest_path, &src_path.symlink_metadata()?)?;
|
||||||
}
|
}
|
||||||
|
staged_paths.insert(rel_path_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy in new files.
|
// Copy in new files.
|
||||||
@@ -708,26 +1054,22 @@ pub fn install_atomic(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(dest_meta) = dest_path.symlink_metadata() {
|
if let Ok(dest_meta) = dest_path.symlink_metadata() {
|
||||||
// lexists checks existence without following symlinks
|
|
||||||
// Backup existing
|
|
||||||
let backup_path = tx.backup_path(&install_rel_path);
|
let backup_path = tx.backup_path(&install_rel_path);
|
||||||
if let Some(parent) = backup_path.parent() {
|
if dest_meta.file_type().is_dir() {
|
||||||
fs::create_dir_all(parent)?;
|
if !remove_set.contains(install_rel_path.as_str()) {
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: if symlink, read link and recreate at backup.
|
|
||||||
if dest_meta.file_type().is_symlink() {
|
|
||||||
let target = fs::read_link(&dest_path)?;
|
|
||||||
std::os::unix::fs::symlink(&target, &backup_path)?;
|
|
||||||
} else if dest_meta.file_type().is_dir() {
|
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"Refusing to replace existing directory with packaged file/symlink: {}",
|
"Refusing to replace existing directory with packaged file/symlink: {}",
|
||||||
install_rel_path
|
install_rel_path
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
fs::copy(&dest_path, &backup_path)?;
|
|
||||||
}
|
}
|
||||||
|
remove_obsolete_children_for_dir(
|
||||||
|
&mut tx,
|
||||||
|
rootfs,
|
||||||
|
&install_rel_path,
|
||||||
|
remove_paths,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
backup_existing_path(&dest_path, &backup_path, &install_rel_path)?;
|
||||||
tx.backed_up.push(install_rel_path.clone());
|
tx.backed_up.push(install_rel_path.clone());
|
||||||
} else {
|
} else {
|
||||||
tx.created.push(install_rel_path.clone());
|
tx.created.push(install_rel_path.clone());
|
||||||
@@ -737,13 +1079,23 @@ pub fn install_atomic(
|
|||||||
// Remove destination if it exists (we backed it up) so we can overwrite
|
// Remove destination if it exists (we backed it up) so we can overwrite
|
||||||
if let Ok(dest_meta) = dest_path.symlink_metadata() {
|
if let Ok(dest_meta) = dest_path.symlink_metadata() {
|
||||||
if dest_meta.file_type().is_dir() {
|
if dest_meta.file_type().is_dir() {
|
||||||
|
if !remove_set.contains(install_rel_path.as_str()) {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"Refusing to replace existing directory with packaged file/symlink: {}",
|
"Refusing to replace existing directory with packaged file/symlink: {}",
|
||||||
install_rel_path
|
install_rel_path
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if !is_directory_empty(&dest_path)? {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Refusing to replace existing non-empty directory with packaged file/symlink: {}",
|
||||||
|
install_rel_path
|
||||||
|
);
|
||||||
|
}
|
||||||
|
fs::remove_dir(&dest_path)?;
|
||||||
|
} else {
|
||||||
fs::remove_file(&dest_path)?;
|
fs::remove_file(&dest_path)?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if file_type.is_symlink() {
|
if file_type.is_symlink() {
|
||||||
let target = fs::read_link(src_path)?;
|
let target = fs::read_link(src_path)?;
|
||||||
@@ -754,45 +1106,15 @@ pub fn install_atomic(
|
|||||||
.with_context(|| format!("Failed to install: {}", install_rel_path))?;
|
.with_context(|| format!("Failed to install: {}", install_rel_path))?;
|
||||||
apply_unix_mode(&dest_path, &metadata)?;
|
apply_unix_mode(&dest_path, &metadata)?;
|
||||||
}
|
}
|
||||||
|
staged_paths.insert(install_rel_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove obsolete files (update only)
|
// Remove obsolete files/directories left behind by the previous version.
|
||||||
for rel in remove_paths {
|
for rel in remove_paths {
|
||||||
let rel = rel.as_str();
|
if staged_paths.contains(rel) || tx.removed.iter().any(|removed| removed == rel) {
|
||||||
let dest_path = rootfs.join(rel);
|
|
||||||
let dest_meta = match dest_path.symlink_metadata() {
|
|
||||||
Ok(m) => m,
|
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
|
|
||||||
Err(e) => {
|
|
||||||
return Err(e).with_context(|| {
|
|
||||||
format!("Failed to inspect obsolete path before removal: {}", rel)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if dest_meta.file_type().is_dir() {
|
|
||||||
// Only obsolete files/symlinks are removed here.
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let _ = backup_and_remove_obsolete_path(&mut tx, rootfs, rel, false)?;
|
||||||
let backup_path = tx.removed_backup_path(rel);
|
|
||||||
if let Some(parent) = backup_path.parent() {
|
|
||||||
fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if dest_meta.file_type().is_symlink() {
|
|
||||||
let target = fs::read_link(&dest_path)
|
|
||||||
.with_context(|| format!("Failed to read obsolete symlink target: {}", rel))?;
|
|
||||||
std::os::unix::fs::symlink(&target, &backup_path)
|
|
||||||
.with_context(|| format!("Failed to backup removed symlink: {}", rel))?;
|
|
||||||
} else {
|
|
||||||
fs::copy(&dest_path, &backup_path)
|
|
||||||
.with_context(|| format!("Failed to backup removed file: {}", rel))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::remove_file(&dest_path)
|
|
||||||
.with_context(|| format!("Failed to remove obsolete file/symlink: {}", rel))?;
|
|
||||||
tx.removed.push(rel.to_string());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -874,6 +1196,72 @@ mod tests {
|
|||||||
assert!(!destdir.join("usr/lib/libfoo.la").exists());
|
assert!(!destdir.join("usr/lib/libfoo.la").exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn process_splits_docs_into_docs_output() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let destdir = tmp.path().join("dest");
|
||||||
|
std::fs::create_dir_all(destdir.join("usr/share/doc/foo")).unwrap();
|
||||||
|
std::fs::create_dir_all(destdir.join("usr/share/gtk-doc/html/foo")).unwrap();
|
||||||
|
std::fs::create_dir_all(destdir.join("opt/foo-docs")).unwrap();
|
||||||
|
std::fs::create_dir_all(destdir.join("usr/bin")).unwrap();
|
||||||
|
std::fs::write(destdir.join("usr/share/doc/foo/README"), "doc").unwrap();
|
||||||
|
std::fs::write(destdir.join("usr/share/gtk-doc/html/foo/index.html"), "gtk").unwrap();
|
||||||
|
std::fs::write(destdir.join("opt/foo-docs/guide.txt"), "guide").unwrap();
|
||||||
|
std::fs::write(destdir.join("usr/bin/foo"), "bin").unwrap();
|
||||||
|
|
||||||
|
let mut spec = mk_spec_for_stage_processing();
|
||||||
|
spec.build.flags.split_docs = true;
|
||||||
|
spec.build.flags.doc_dirs = vec!["/opt/foo-docs".to_string()];
|
||||||
|
|
||||||
|
process(&destdir, &spec).unwrap();
|
||||||
|
|
||||||
|
let docs_destdir = output_staging_dir(&destdir, "foo-docs");
|
||||||
|
assert!(docs_destdir.join("usr/share/doc/foo/README").exists());
|
||||||
|
assert!(
|
||||||
|
docs_destdir
|
||||||
|
.join("usr/share/gtk-doc/html/foo/index.html")
|
||||||
|
.exists()
|
||||||
|
);
|
||||||
|
assert!(docs_destdir.join("opt/foo-docs/guide.txt").exists());
|
||||||
|
assert!(destdir.join("usr/bin/foo").exists());
|
||||||
|
assert!(!destdir.join("usr/share/doc/foo/README").exists());
|
||||||
|
assert!(
|
||||||
|
!destdir
|
||||||
|
.join("usr/share/gtk-doc/html/foo/index.html")
|
||||||
|
.exists()
|
||||||
|
);
|
||||||
|
assert!(!destdir.join("opt/foo-docs/guide.txt").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn process_splits_docs_for_additional_outputs() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let destdir = tmp.path().join("dest");
|
||||||
|
let dev_destdir = output_staging_dir(&destdir, "foo-dev");
|
||||||
|
std::fs::create_dir_all(dev_destdir.join("usr/share/doc/foo-dev")).unwrap();
|
||||||
|
std::fs::create_dir_all(dev_destdir.join("usr/include")).unwrap();
|
||||||
|
std::fs::write(dev_destdir.join("usr/share/doc/foo-dev/README"), "doc").unwrap();
|
||||||
|
std::fs::write(dev_destdir.join("usr/include/foo.h"), "header").unwrap();
|
||||||
|
|
||||||
|
let mut spec = mk_spec_for_stage_processing();
|
||||||
|
spec.packages.push(PackageInfo {
|
||||||
|
name: "foo-dev".into(),
|
||||||
|
version: "1.0".into(),
|
||||||
|
revision: 1,
|
||||||
|
description: "dev".into(),
|
||||||
|
homepage: "h".into(),
|
||||||
|
license: vec!["MIT".into()],
|
||||||
|
});
|
||||||
|
spec.build.flags.split_docs = true;
|
||||||
|
|
||||||
|
process(&destdir, &spec).unwrap();
|
||||||
|
|
||||||
|
let docs_destdir = output_staging_dir(&destdir, "foo-dev-docs");
|
||||||
|
assert!(docs_destdir.join("usr/share/doc/foo-dev/README").exists());
|
||||||
|
assert!(dev_destdir.join("usr/include/foo.h").exists());
|
||||||
|
assert!(!dev_destdir.join("usr/share/doc/foo-dev/README").exists());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn add_licenses_copies_common_files() {
|
fn add_licenses_copies_common_files() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
@@ -1056,6 +1444,43 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn install_atomic_replaces_obsolete_directory_with_symlink() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let rootfs = tmp.path().join("root");
|
||||||
|
let destdir = tmp.path().join("dest");
|
||||||
|
let tx_base = tmp.path().join("tx");
|
||||||
|
std::fs::create_dir_all(rootfs.join("usr/sbin")).unwrap();
|
||||||
|
std::fs::create_dir_all(destdir.join("usr/bin")).unwrap();
|
||||||
|
std::fs::create_dir_all(destdir.join("usr")).unwrap();
|
||||||
|
std::fs::write(rootfs.join("usr/sbin/legacy"), "old").unwrap();
|
||||||
|
std::fs::write(destdir.join("usr/bin/legacy"), "new").unwrap();
|
||||||
|
std::os::unix::fs::symlink("bin", destdir.join("usr/sbin")).unwrap();
|
||||||
|
|
||||||
|
let remove_paths = vec!["usr/sbin/legacy".to_string(), "usr/sbin".to_string()];
|
||||||
|
let tx = install_atomic(&destdir, &rootfs, &tx_base, &remove_paths, &[]).unwrap();
|
||||||
|
|
||||||
|
let sbin_meta = rootfs.join("usr/sbin").symlink_metadata().unwrap();
|
||||||
|
assert!(sbin_meta.file_type().is_symlink());
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_link(rootfs.join("usr/sbin")).unwrap(),
|
||||||
|
PathBuf::from("bin")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(rootfs.join("usr/bin/legacy")).unwrap(),
|
||||||
|
"new"
|
||||||
|
);
|
||||||
|
|
||||||
|
tx.rollback().unwrap();
|
||||||
|
let restored = rootfs.join("usr/sbin").symlink_metadata().unwrap();
|
||||||
|
assert!(restored.file_type().is_dir());
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(rootfs.join("usr/sbin/legacy")).unwrap(),
|
||||||
|
"old"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn keep_glob_matches_question_mark_and_not_path_separator() {
|
fn keep_glob_matches_question_mark_and_not_path_separator() {
|
||||||
assert!(glob_match_path(
|
assert!(glob_match_path(
|
||||||
|
|||||||
Reference in New Issue
Block a user