diff --git a/src/combiner.rs b/src/combiner.rs index 006a40c..4c47024 100644 --- a/src/combiner.rs +++ b/src/combiner.rs @@ -7,6 +7,7 @@ use crate::{ use anyhow::{bail, Context, Result}; use std::io::{Read, Write}; use std::path::Path; +use std::path::PathBuf; use tar::Archive; actor! { @@ -34,10 +35,10 @@ actor! { non_installed_overlay: String = "", /// The directory to do temporary work. - work_dir: String = "./workdir", + work_dir: PathBuf = "./workdir", /// The location to put the final image and tarball. - output_dir: String = "./dist", + output_dir: PathBuf = "./dist", /// The formats used to compress the tarball compression_formats: CompressionFormats = CompressionFormats::default(), @@ -49,7 +50,7 @@ impl Combiner { pub fn run(self) -> Result<()> { create_dir_all(&self.work_dir)?; - let package_dir = Path::new(&self.work_dir).join(&self.package_name); + let package_dir = self.work_dir.join(&self.package_name); if package_dir.exists() { remove_dir_all(&package_dir)?; } @@ -73,14 +74,14 @@ impl Combiner { .with_context(|| { format!( "unable to extract '{}' into '{}'", - &input_tarball, self.work_dir + &input_tarball, self.work_dir.display() ) })?; let pkg_name = input_tarball.trim_end_matches(&format!(".tar.{}", compression.extension())); let pkg_name = Path::new(pkg_name).file_name().unwrap(); - let pkg_dir = Path::new(&self.work_dir).join(&pkg_name); + let pkg_dir = self.work_dir.join(&pkg_name); // Verify the version number. let mut version = String::new(); @@ -137,10 +138,10 @@ impl Combiner { // Make the tarballs. create_dir_all(&self.output_dir)?; - let output = Path::new(&self.output_dir).join(&self.package_name); + let output = self.output_dir.join(&self.package_name); let mut tarballer = Tarballer::default(); tarballer - .work_dir(self.work_dir) + .work_dir(LongPath::new(self.work_dir)) .input(self.package_name) .output(path_to_str(&output)?.into()) .compression_formats(self.compression_formats.clone()); diff --git a/src/generator.rs b/src/generator.rs index 2601eb5..c198f50 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -34,13 +34,13 @@ actor! { bulk_dirs: String = "", /// The directory containing the installation medium - image_dir: String = "./install_image", + image_dir: LongPath = "./install_image", /// The directory to do temporary work - work_dir: String = "./workdir", + work_dir: LongPath = "./workdir", /// The location to put the final image and tarball - output_dir: String = "./dist", + output_dir: LongPath = "./dist", /// The formats used to compress the tarball compression_formats: CompressionFormats = CompressionFormats::default(), @@ -50,9 +50,9 @@ actor! { impl Generator { /// Generates the actual installer tarball pub fn run(self) -> Result<()> { - create_dir_all(&self.work_dir)?; + create_dir_all(&*self.work_dir)?; - let package_dir = Path::new(&self.work_dir).join(&self.package_name); + let package_dir = self.work_dir.join(&self.package_name); if package_dir.exists() { remove_dir_all(&package_dir)?; } @@ -78,7 +78,8 @@ impl Generator { // Copy the overlay if !self.non_installed_overlay.is_empty() { - copy_recursive(self.non_installed_overlay.as_ref(), &package_dir)?; + copy_recursive(self.non_installed_overlay.as_ref(), &package_dir) + .context("failed to copy overlay")?; } // Generate the install script @@ -93,8 +94,8 @@ impl Generator { scripter.run()?; // Make the tarballs - create_dir_all(&self.output_dir)?; - let output = Path::new(&self.output_dir).join(&self.package_name); + create_dir_all(&*self.output_dir)?; + let output = self.output_dir.join(&self.package_name); let mut tarballer = Tarballer::default(); tarballer .work_dir(self.work_dir) diff --git a/src/tarballer.rs b/src/tarballer.rs index 4ac8cf7..4b4891e 100644 --- a/src/tarballer.rs +++ b/src/tarballer.rs @@ -20,7 +20,7 @@ actor! { output: String = "./dist", /// The folder in which the input is to be found. - work_dir: String = "./workdir", + work_dir: LongPath = "./workdir", /// The formats used to compress the tarball. compression_formats: CompressionFormats = CompressionFormats::default(), @@ -50,19 +50,16 @@ impl Tarballer { let buf = BufWriter::with_capacity(1024 * 1024, encoder); let mut builder = Builder::new(buf); - let pool = rayon::ThreadPoolBuilder::new() - .num_threads(2) - .build() - .unwrap(); + let pool = rayon::ThreadPoolBuilder::new().num_threads(2).build().unwrap(); pool.install(move || { for path in dirs { - let src = Path::new(&self.work_dir).join(&path); + let src = self.work_dir.join(&path); builder .append_dir(&path, &src) .with_context(|| format!("failed to tar dir '{}'", src.display()))?; } for path in files { - let src = Path::new(&self.work_dir).join(&path); + let src = self.work_dir.join(&path); append_path(&mut builder, &src, &path) .with_context(|| format!("failed to tar file '{}'", src.display()))?; } @@ -106,20 +103,11 @@ fn append_path(builder: &mut Builder, src: &Path, path: &String) -> } /// Returns all `(directories, files)` under the source path. -fn get_recursive_paths(root: P, name: Q) -> Result<(Vec, Vec)> -where - P: AsRef, - Q: AsRef, -{ - let root = root.as_ref(); +fn get_recursive_paths(root: &Path, name: impl AsRef) -> Result<(Vec, Vec)> { let name = name.as_ref(); if !name.is_relative() && !name.starts_with(root) { - bail!( - "input '{}' is not in work dir '{}'", - name.display(), - root.display() - ); + bail!("input '{}' is not in work dir '{}'", name.display(), root.display()); } let mut dirs = vec![]; diff --git a/src/util.rs b/src/util.rs index 078ceb3..c8ee161 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,6 +1,8 @@ use anyhow::{format_err, Context, Result}; +use std::ffi::OsString; use std::fs; -use std::path::Path; +use std::ops::Deref; +use std::path::{Component, Path, PathBuf, Prefix}; use walkdir::WalkDir; // Needed to set the script mode to executable. @@ -15,8 +17,7 @@ use std::os::windows::fs::symlink_file; /// Converts a `&Path` to a UTF-8 `&str`. pub fn path_to_str(path: &Path) -> Result<&str> { - path.to_str() - .ok_or_else(|| format_err!("path is not valid UTF-8 '{}'", path.display())) + path.to_str().ok_or_else(|| format_err!("path is not valid UTF-8 '{}'", path.display())) } /// Wraps `fs::copy` with a nicer error message. @@ -28,9 +29,16 @@ pub fn copy, Q: AsRef>(from: P, to: Q) -> Result { } else { let amt = fs::copy(&from, &to).with_context(|| { format!( - "failed to copy '{}' to '{}'", + "failed to copy '{}' ({}) to '{}' ({}, parent {})", from.as_ref().display(), - to.as_ref().display() + if from.as_ref().exists() { "exists" } else { "doesn't exist" }, + to.as_ref().display(), + if to.as_ref().exists() { "exists" } else { "doesn't exist" }, + if to.as_ref().parent().unwrap_or_else(|| Path::new("")).exists() { + "exists" + } else { + "doesn't exist" + }, ) })?; Ok(amt) @@ -97,7 +105,20 @@ pub fn remove_file>(path: P) -> Result<()> { /// Copies the `src` directory recursively to `dst`. Both are assumed to exist /// when this function is called. pub fn copy_recursive(src: &Path, dst: &Path) -> Result<()> { - copy_with_callback(src, dst, |_, _| Ok(())) + copy_with_callback(src, dst, |_, _| Ok(())).with_context(|| { + format!( + "failed to recursively copy '{}' ({}) to '{}' ({}, parent {})", + src.display(), + if src.exists() { "exists" } else { "doesn't exist" }, + dst.display(), + if dst.exists() { "exists" } else { "doesn't exist" }, + if dst.parent().unwrap_or_else(|| Path::new("")).exists() { + "exists" + } else { + "doesn't exist" + }, + ) + }) } /// Copies the `src` directory recursively to `dst`. Both are assumed to exist @@ -122,6 +143,89 @@ where Ok(()) } +fn normalize_rest(path: PathBuf) -> PathBuf { + let mut new_components = vec![]; + for component in path.components().skip(1) { + match component { + Component::Prefix(_) => unreachable!(), + Component::RootDir => new_components.clear(), + Component::CurDir => {} + Component::ParentDir => { + new_components.pop(); + } + Component::Normal(component) => new_components.push(component), + } + } + new_components.into_iter().collect() +} + +#[derive(Debug)] +pub struct LongPath(PathBuf); + +impl LongPath { + pub fn new(path: PathBuf) -> Self { + let path = if cfg!(windows) { + // Convert paths to verbatim paths to ensure that paths longer than 255 characters work + match dbg!(path.components().next().unwrap()) { + Component::Prefix(prefix_component) => { + match prefix_component.kind() { + Prefix::Verbatim(_) + | Prefix::VerbatimUNC(_, _) + | Prefix::VerbatimDisk(_) => { + // Already a verbatim path. + path + } + + Prefix::DeviceNS(dev) => { + let mut base = OsString::from("\\\\?\\"); + base.push(dev); + Path::new(&base).join(normalize_rest(path)) + } + Prefix::UNC(host, share) => { + let mut base = OsString::from("\\\\?\\UNC\\"); + base.push(host); + base.push("\\"); + base.push(share); + Path::new(&base).join(normalize_rest(path)) + } + Prefix::Disk(_disk) => { + let mut base = OsString::from("\\\\?\\"); + base.push(prefix_component.as_os_str()); + Path::new(&base).join(normalize_rest(path)) + } + } + } + + Component::RootDir + | Component::CurDir + | Component::ParentDir + | Component::Normal(_) => { + return LongPath::new(dbg!( + std::env::current_dir().expect("failed to get current dir").join(&path) + )); + } + } + } else { + path + }; + LongPath(dbg!(path)) + } +} + +impl Into for &str { + fn into(self) -> LongPath { + LongPath::new(self.into()) + } +} + +impl Deref for LongPath { + type Target = Path; + + fn deref(&self) -> &Path { + &self.0 + } +} + /// Creates an "actor" with default values and setters for all fields. macro_rules! actor { ($( #[ $attr:meta ] )+ pub struct $name:ident {