diff --git a/crates/turborepo-lib/src/cli.rs b/crates/turborepo-lib/src/cli.rs index d97f86769e231..9a802782b88c6 100644 --- a/crates/turborepo-lib/src/cli.rs +++ b/crates/turborepo-lib/src/cli.rs @@ -1,6 +1,5 @@ use std::{ - env::{self, current_dir}, - io, mem, + env, io, mem, path::{Path, PathBuf}, process, }; @@ -8,7 +7,6 @@ use std::{ use anyhow::{anyhow, Result}; use clap::{ArgAction, CommandFactory, Parser, Subcommand, ValueEnum}; use clap_complete::{generate, Shell}; -use dunce::canonicalize as fs_canonicalize; use serde::Serialize; use tracing::{debug, error}; use turbopath::AbsoluteSystemPathBuf; @@ -559,10 +557,10 @@ pub async fn run( let invocation_path = Path::new(&invocation_dir); // If repo state doesn't exist, we're either local turbo running at the root - // (current_dir), or inference failed If repo state does exist, + // (cwd), or inference failed If repo state does exist, // we're global turbo, and want to calculate package inference based on the repo // root - let this_dir = current_dir()?; + let this_dir = AbsoluteSystemPathBuf::cwd()?; let repo_root = repo_state.as_ref().map(|r| &r.root).unwrap_or(&this_dir); if let Ok(relative_path) = invocation_path.strip_prefix(repo_root) { debug!("pkg_inference_root set to \"{}\"", relative_path.display()); @@ -582,21 +580,15 @@ pub async fn run( if let Some(Command::Run(run_args)) = &mut cli_args.command { run_args.single_package = matches!(repo_state.mode, RepoMode::SinglePackage); } - cli_args.cwd = Some(repo_state.root); + cli_args.cwd = Some(repo_state.root.as_path().to_owned()); } let repo_root = if let Some(cwd) = &cli_args.cwd { - let canonical_cwd = fs_canonicalize(cwd)?; - // Update on clap_args so that Go gets a canonical path. - cli_args.cwd = Some(canonical_cwd.clone()); - canonical_cwd + AbsoluteSystemPathBuf::from_cwd(cwd)? } else { - current_dir()? + AbsoluteSystemPathBuf::cwd()? }; - // a non-absolute repo root is a bug - let repo_root = AbsoluteSystemPathBuf::new(repo_root).expect("repo_root is not absolute"); - let version = get_version(); match cli_args.command.as_ref().unwrap() { diff --git a/crates/turborepo-lib/src/package_manager/mod.rs b/crates/turborepo-lib/src/package_manager/mod.rs index c3ecbb285868c..0ffb8246402ef 100644 --- a/crates/turborepo-lib/src/package_manager/mod.rs +++ b/crates/turborepo-lib/src/package_manager/mod.rs @@ -11,6 +11,7 @@ use anyhow::{anyhow, Result}; use itertools::Itertools; use regex::Regex; use serde::{Deserialize, Serialize}; +use turbopath::AbsoluteSystemPath; use crate::{ commands::CommandBase, @@ -85,7 +86,7 @@ pub struct Globs { } impl Globs { - pub fn test(&self, root: PathBuf, target: PathBuf) -> Result { + pub fn test(&self, root: &Path, target: PathBuf) -> Result { let search_value = target .strip_prefix(root)? .to_str() @@ -121,10 +122,11 @@ impl PackageManager { /// /// ``` /// ``` - pub fn get_workspace_globs(&self, root_path: &Path) -> Result> { + pub fn get_workspace_globs(&self, root_path: &AbsoluteSystemPath) -> Result> { let globs = match self { PackageManager::Pnpm | PackageManager::Pnpm6 => { - let workspace_yaml = fs::read_to_string(root_path.join("pnpm-workspace.yaml"))?; + let workspace_yaml = + fs::read_to_string(root_path.join_component("pnpm-workspace.yaml"))?; let pnpm_workspace: PnpmWorkspace = serde_yaml::from_str(&workspace_yaml)?; if pnpm_workspace.packages.is_empty() { return Ok(None); @@ -133,7 +135,8 @@ impl PackageManager { } } PackageManager::Berry | PackageManager::Npm | PackageManager::Yarn => { - let package_json_text = fs::read_to_string(root_path.join("package.json"))?; + let package_json_text = + fs::read_to_string(root_path.join_component("package.json"))?; let package_json: PackageJsonWorkspaces = serde_json::from_str(&package_json_text)?; if package_json.workspaces.as_ref().is_empty() { @@ -237,7 +240,7 @@ impl PackageManager { #[cfg(test)] mod tests { - use std::{fs::File, path::Path}; + use std::fs::File; use tempfile::tempdir; use turbopath::AbsoluteSystemPathBuf; @@ -393,9 +396,15 @@ mod tests { #[test] fn test_get_workspace_globs() { + let cwd = AbsoluteSystemPathBuf::cwd().unwrap(); + let repo_root = cwd + .ancestors() + .find(|path| path.join_component(".git").exists()) + .unwrap(); + let with_yarn = repo_root.join_components(&["examples", "with-yarn"]); let package_manager = PackageManager::Npm; let globs = package_manager - .get_workspace_globs(Path::new("../../examples/with-yarn")) + .get_workspace_globs(with_yarn.as_absolute_path()) .unwrap() .unwrap(); @@ -422,7 +431,7 @@ mod tests { }]; for test in tests { - match test.globs.test(test.root, test.target) { + match test.globs.test(&test.root, test.target) { Ok(value) => assert_eq!(value, test.output.unwrap()), Err(value) => assert_eq!(value.to_string(), test.output.unwrap_err().to_string()), }; diff --git a/crates/turborepo-lib/src/shim.rs b/crates/turborepo-lib/src/shim.rs index 639bb8d3d4233..1e8e0da88abb5 100644 --- a/crates/turborepo-lib/src/shim.rs +++ b/crates/turborepo-lib/src/shim.rs @@ -1,6 +1,5 @@ use std::{ env, - env::current_dir, ffi::OsString, fs::{self}, path::{Path, PathBuf}, @@ -17,6 +16,7 @@ use serde::{Deserialize, Serialize}; use tiny_gradient::{GradientStr, RGB}; use tracing::debug; use turbo_updater::check_for_updates; +use turbopath::{AbsoluteSystemPath, AbsoluteSystemPathBuf}; use crate::{ cli, get_version, package_manager::Globs, spawn_child, tracing::TurboSubscriber, ui::UI, @@ -50,8 +50,8 @@ fn turbo_version_has_shim(version: &str) -> bool { #[derive(Debug)] struct ShimArgs { - cwd: PathBuf, - invocation_dir: PathBuf, + cwd: AbsoluteSystemPathBuf, + invocation_dir: AbsoluteSystemPathBuf, skip_infer: bool, verbosity: usize, force_update_check: bool, @@ -64,7 +64,7 @@ struct ShimArgs { impl ShimArgs { pub fn parse() -> Result { let mut found_cwd_flag = false; - let mut cwd: Option = None; + let mut cwd: Option = None; let mut skip_infer = false; let mut found_verbosity_flag = false; let mut verbosity = 0; @@ -102,7 +102,8 @@ impl ShimArgs { verbosity = arg[1..].len(); } else if found_cwd_flag { // We've seen a `--cwd` and therefore set the cwd to this arg. - cwd = Some(arg.into()); + //cwd = Some(arg.into()); + cwd = Some(AbsoluteSystemPathBuf::from_cwd(arg)?); found_cwd_flag = false; } else if arg == "--cwd" { if cwd.is_some() { @@ -116,7 +117,7 @@ impl ShimArgs { if cwd.is_some() { return Err(anyhow!("cannot have multiple `--cwd` flags in command")); } - cwd = Some(cwd_arg.into()); + cwd = Some(AbsoluteSystemPathBuf::from_cwd(cwd_arg)?); } else if arg == "--color" { color = true; } else if arg == "--no-color" { @@ -129,12 +130,8 @@ impl ShimArgs { if found_cwd_flag { Err(anyhow!("No value assigned to `--cwd` argument")) } else { - let invocation_dir = current_dir()?; - let cwd = if let Some(cwd) = cwd { - fs_canonicalize(cwd)? - } else { - invocation_dir.clone() - }; + let invocation_dir = AbsoluteSystemPathBuf::cwd()?; + let cwd = cwd.unwrap_or_else(|| invocation_dir.clone()); Ok(ShimArgs { cwd, @@ -212,7 +209,7 @@ impl Default for YarnRc { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct TurboState { bin_path: Option, version: &'static str, @@ -425,16 +422,16 @@ impl LocalTurboState { } } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Serialize)] pub struct RepoState { - pub root: PathBuf, + pub root: AbsoluteSystemPathBuf, pub mode: RepoMode, pub local_turbo_state: Option, } #[derive(Debug)] struct InferInfo { - path: PathBuf, + path: AbsoluteSystemPathBuf, has_package_json: bool, has_turbo_json: bool, workspace_globs: Option, @@ -451,7 +448,7 @@ impl InferInfo { pub fn is_workspace_root_of(&self, target_path: &Path) -> bool { match &self.workspace_globs { Some(globs) => globs - .test(self.path.to_path_buf(), target_path.to_path_buf()) + .test(self.path.as_path(), target_path.to_path_buf()) .unwrap_or(false), None => false, } @@ -459,14 +456,14 @@ impl InferInfo { } impl RepoState { - fn generate_potential_turbo_roots(reference_dir: &Path) -> Vec { + fn generate_potential_turbo_roots(reference_dir: &AbsoluteSystemPath) -> Vec { // Find all directories that contain a `package.json` or a `turbo.json`. // Gather a bit of additional metadata about them. let potential_turbo_roots = reference_dir .ancestors() .filter_map(|path| { - let has_package_json = fs::metadata(path.join("package.json")).is_ok(); - let has_turbo_json = fs::metadata(path.join("turbo.json")).is_ok(); + let has_package_json = path.join_component("package.json").exists(); + let has_turbo_json = path.join_component("turbo.json").exists(); if !has_package_json && !has_turbo_json { return None; @@ -529,9 +526,9 @@ impl RepoState { // If there is only one potential root, that's the winner. if check_roots.peek().is_none() { - let local_turbo_state = LocalTurboState::infer(¤t.path); + let local_turbo_state = LocalTurboState::infer(current.path.as_path()); return Ok(Self { - root: current.path.to_path_buf(), + root: current.path.clone(), mode: if current.workspace_globs.is_some() { RepoMode::MultiPackage } else { @@ -545,9 +542,9 @@ impl RepoState { // and set the mode properly in the else and it would still work. } else if current.workspace_globs.is_some() { // If the closest one has workspaces then we stop there. - let local_turbo_state = LocalTurboState::infer(¤t.path); + let local_turbo_state = LocalTurboState::infer(current.path.as_path()); return Ok(Self { - root: current.path.to_path_buf(), + root: current.path.clone(), mode: RepoMode::MultiPackage, local_turbo_state, }); @@ -558,10 +555,11 @@ impl RepoState { // Failing that we just choose the closest. } else { for ancestor_infer in check_roots { - if ancestor_infer.is_workspace_root_of(¤t.path) { - let local_turbo_state = LocalTurboState::infer(&ancestor_infer.path); + if ancestor_infer.is_workspace_root_of(current.path.as_path()) { + let local_turbo_state = + LocalTurboState::infer(ancestor_infer.path.as_path()); return Ok(Self { - root: ancestor_infer.path.to_path_buf(), + root: ancestor_infer.path.clone(), mode: RepoMode::MultiPackage, local_turbo_state, }); @@ -570,9 +568,9 @@ impl RepoState { // We have eliminated RepoMode::MultiPackage as an option. // We must exhaustively check before this becomes the answer. - let local_turbo_state = LocalTurboState::infer(¤t.path); + let local_turbo_state = LocalTurboState::infer(current.path.as_path()); return Ok(Self { - root: current.path.to_path_buf(), + root: current.path.clone(), mode: RepoMode::SinglePackage, local_turbo_state, }); @@ -590,7 +588,7 @@ impl RepoState { /// * `current_dir`: Current working directory /// /// returns: Result - pub fn infer(reference_dir: &Path) -> Result { + pub fn infer(reference_dir: &AbsoluteSystemPath) -> Result { let potential_turbo_roots = RepoState::generate_potential_turbo_roots(reference_dir); RepoState::process_potential_turbo_roots(potential_turbo_roots) } @@ -621,7 +619,10 @@ impl RepoState { try_check_for_updates(&shim_args, get_version()); // cli::run checks for this env var, rather than an arg, so that we can support // calling old versions without passing unknown flags. - env::set_var(cli::INVOCATION_DIR_ENV_VAR, &shim_args.invocation_dir); + env::set_var( + cli::INVOCATION_DIR_ENV_VAR, + shim_args.invocation_dir.as_path(), + ); debug!("Running command as global turbo"); cli::run(Some(self), subscriber, ui) } @@ -679,7 +680,10 @@ impl RepoState { .args(&raw_args) // rather than passing an argument that local turbo might not understand, set // an environment variable that can be optionally used - .env(cli::INVOCATION_DIR_ENV_VAR, &shim_args.invocation_dir) + .env( + cli::INVOCATION_DIR_ENV_VAR, + shim_args.invocation_dir.as_path(), + ) .current_dir(cwd) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()); @@ -749,12 +753,12 @@ pub fn run() -> Result { // it to execute local turbo. We simply use it to set the `--single-package` // and `--cwd` flags. if is_turbo_binary_path_set() { - let repo_state = RepoState::infer(&args.cwd)?; + let repo_state = RepoState::infer(args.cwd.as_absolute_path())?; debug!("Repository Root: {}", repo_state.root.to_string_lossy()); return cli::run(Some(repo_state), &subscriber, ui); } - match RepoState::infer(&args.cwd) { + match RepoState::infer(args.cwd.as_absolute_path()) { Ok(repo_state) => { debug!("Repository Root: {}", repo_state.root.to_string_lossy()); repo_state.run_correct_turbo(args, &subscriber, ui) @@ -773,14 +777,29 @@ pub fn run() -> Result { mod test { use super::*; + fn tmp_dir() -> (tempfile::TempDir, AbsoluteSystemPathBuf) { + let tmp_dir = tempfile::tempdir().unwrap(); + let dir = AbsoluteSystemPathBuf::new(tmp_dir.path().to_path_buf()) + .unwrap() + .to_realpath() + .unwrap(); + (tmp_dir, dir) + } + #[test] fn test_process_potential_turbo_roots() { struct TestCase { description: &'static str, infer_infos: Vec, - output: Result, + output: Result, } + let (_tmp, root) = tmp_dir(); + let root_one = root.join_components(&["..", "root-one"]); + let root_two = root_one.join_component("root-two"); + let project_one = root.join_components(&["..", "project-one"]); + let project_two = project_one.join_component("project-two"); + let tests = [ // Test for zero, exhaustive. TestCase { @@ -792,7 +811,7 @@ mod test { TestCase { description: "Only one, is monorepo with turbo.json.", infer_infos: vec![InferInfo { - path: PathBuf::from("/path/to/root"), + path: root.clone(), has_package_json: true, has_turbo_json: true, workspace_globs: Some(Globs { @@ -800,22 +819,22 @@ mod test { exclusions: vec![], }), }], - output: Ok(PathBuf::from("/path/to/root")), + output: Ok(root.clone()), }, TestCase { description: "Only one, is non-monorepo with turbo.json.", infer_infos: vec![InferInfo { - path: PathBuf::from("/path/to/root"), + path: root.clone(), has_package_json: true, has_turbo_json: true, workspace_globs: None, }], - output: Ok(PathBuf::from("/path/to/root")), + output: Ok(root.clone()), }, TestCase { description: "Only one, is monorepo without turbo.json.", infer_infos: vec![InferInfo { - path: PathBuf::from("/path/to/root"), + path: root.clone(), has_package_json: true, has_turbo_json: false, workspace_globs: Some(Globs { @@ -823,30 +842,30 @@ mod test { exclusions: vec![], }), }], - output: Ok(PathBuf::from("/path/to/root")), + output: Ok(root.clone()), }, TestCase { description: "Only one, is non-monorepo without turbo.json.", infer_infos: vec![InferInfo { - path: PathBuf::from("/path/to/root"), + path: root.clone(), has_package_json: true, has_turbo_json: false, workspace_globs: None, }], - output: Ok(PathBuf::from("/path/to/root")), + output: Ok(root.clone()), }, // Tests for how to choose what is closest. TestCase { description: "Execution in a workspace.", infer_infos: vec![ InferInfo { - path: PathBuf::from("/path/to/root/packages/ui-library"), + path: root.join_components(&["packages", "ui-library"]), has_package_json: true, has_turbo_json: true, workspace_globs: None, }, InferInfo { - path: PathBuf::from("/path/to/root"), + path: root.clone(), has_package_json: true, has_turbo_json: true, workspace_globs: Some(Globs { @@ -855,25 +874,25 @@ mod test { }), }, ], - output: Ok(PathBuf::from("/path/to/root")), + output: Ok(root.clone()), }, TestCase { description: "Execution in a workspace, weird package layout.", infer_infos: vec![ InferInfo { - path: PathBuf::from("/path/to/root/packages/ui-library/css"), + path: root.join_components(&["packages", "ui-library", "css"]), has_package_json: true, has_turbo_json: true, workspace_globs: None, }, InferInfo { - path: PathBuf::from("/path/to/root/packages/ui-library"), + path: root.join_components(&["packages", "ui-library"]), has_package_json: true, has_turbo_json: true, workspace_globs: None, }, InferInfo { - path: PathBuf::from("/path/to/root"), + path: root.clone(), has_package_json: true, has_turbo_json: true, workspace_globs: Some(Globs { @@ -883,13 +902,13 @@ mod test { }), }, ], - output: Ok(PathBuf::from("/path/to/root")), + output: Ok(root.clone()), }, TestCase { description: "Nested disjoint monorepo roots.", infer_infos: vec![ InferInfo { - path: PathBuf::from("/path/to/root-one/root-two"), + path: root_two.clone(), has_package_json: true, has_turbo_json: true, workspace_globs: Some(Globs { @@ -898,7 +917,7 @@ mod test { }), }, InferInfo { - path: PathBuf::from("/path/to/root-one"), + path: root_one.clone(), has_package_json: true, has_turbo_json: true, workspace_globs: Some(Globs { @@ -907,22 +926,20 @@ mod test { }), }, ], - output: Ok(PathBuf::from("/path/to/root-one/root-two")), + output: Ok(root_two.clone()), }, TestCase { description: "Nested disjoint monorepo roots, execution in a workspace of the \ closer root.", infer_infos: vec![ InferInfo { - path: PathBuf::from( - "/path/to/root-one/root-two/root-two-packages/ui-library", - ), + path: root_two.join_components(&["root-two-packages", "ui-library"]), has_package_json: true, has_turbo_json: true, workspace_globs: None, }, InferInfo { - path: PathBuf::from("/path/to/root-one/root-two"), + path: root_two.clone(), has_package_json: true, has_turbo_json: true, workspace_globs: Some(Globs { @@ -931,7 +948,7 @@ mod test { }), }, InferInfo { - path: PathBuf::from("/path/to/root-one"), + path: root_one.clone(), has_package_json: true, has_turbo_json: true, workspace_globs: Some(Globs { @@ -940,22 +957,20 @@ mod test { }), }, ], - output: Ok(PathBuf::from("/path/to/root-one/root-two")), + output: Ok(root_two.clone()), }, TestCase { description: "Nested disjoint monorepo roots, execution in a workspace of the \ farther root.", infer_infos: vec![ InferInfo { - path: PathBuf::from( - "/path/to/root-one/root-two/root-one-packages/ui-library", - ), + path: root_two.join_components(&["root-one-packages", "ui-library"]), has_package_json: true, has_turbo_json: true, workspace_globs: None, }, InferInfo { - path: PathBuf::from("/path/to/root-one/root-two"), + path: root_two.clone(), has_package_json: true, has_turbo_json: true, workspace_globs: Some(Globs { @@ -964,7 +979,7 @@ mod test { }), }, InferInfo { - path: PathBuf::from("/path/to/root-one"), + path: root_one.clone(), has_package_json: true, has_turbo_json: true, workspace_globs: Some(Globs { @@ -973,19 +988,19 @@ mod test { }), }, ], - output: Ok(PathBuf::from("/path/to/root-one")), + output: Ok(root_one.clone()), }, TestCase { description: "Disjoint package.", infer_infos: vec![ InferInfo { - path: PathBuf::from("/path/to/root/some-other-project"), + path: root.join_component("some-other-project"), has_package_json: true, has_turbo_json: true, workspace_globs: None, }, InferInfo { - path: PathBuf::from("/path/to/root"), + path: root.clone(), has_package_json: true, has_turbo_json: true, workspace_globs: Some(Globs { @@ -994,14 +1009,14 @@ mod test { }), }, ], - output: Ok(PathBuf::from("/path/to/root/some-other-project")), + output: Ok(root.join_component("some-other-project")), }, TestCase { description: "Monorepo trying to point to a monorepo. We choose the closer one \ and ignore the problem.", infer_infos: vec![ InferInfo { - path: PathBuf::from("/path/to/root-one/root-two"), + path: root_two.clone(), has_package_json: true, has_turbo_json: true, workspace_globs: Some(Globs { @@ -1010,7 +1025,7 @@ mod test { }), }, InferInfo { - path: PathBuf::from("/path/to/root-one"), + path: root_one.clone(), has_package_json: true, has_turbo_json: true, workspace_globs: Some(Globs { @@ -1019,25 +1034,25 @@ mod test { }), }, ], - output: Ok(PathBuf::from("/path/to/root-one/root-two")), + output: Ok(root_two.clone()), }, TestCase { description: "Nested non-monorepo packages.", infer_infos: vec![ InferInfo { - path: PathBuf::from("/path/to/project-one/project-two"), + path: project_two.clone(), has_package_json: true, has_turbo_json: true, workspace_globs: None, }, InferInfo { - path: PathBuf::from("/path/to/project-one"), + path: project_one.clone(), has_package_json: true, has_turbo_json: true, workspace_globs: None, }, ], - output: Ok(PathBuf::from("/path/to/project-one/project-two")), + output: Ok(project_two.clone()), }, // The below test ensures that we privilege a valid `turbo.json` structure prior to // evaluation of a valid `package.json` structure. If you include `turbo.json` you are @@ -1052,19 +1067,19 @@ mod test { description: "Nested non-monorepo packages, turbo.json primacy.", infer_infos: vec![ InferInfo { - path: PathBuf::from("/path/to/project-one/project-two"), + path: project_two.clone(), has_package_json: true, has_turbo_json: false, workspace_globs: None, }, InferInfo { - path: PathBuf::from("/path/to/project-one"), + path: project_one.clone(), has_package_json: true, has_turbo_json: true, workspace_globs: None, }, ], - output: Ok(PathBuf::from("/path/to/project-one")), + output: Ok(project_one), }, ]; @@ -1106,14 +1121,4 @@ mod test { assert!(!turbo_version_has_shim(old)); assert!(!turbo_version_has_shim(old_canary)); } - - #[cfg(windows)] - #[test] - fn test_windows_path_normalization() -> Result<()> { - let cwd = current_dir()?; - let normalized = fs_canonicalize(&cwd)?; - // Just make sure it isn't a UNC path - assert!(!normalized.starts_with("\\\\?")); - Ok(()) - } } diff --git a/crates/turborepo-paths/src/absolute_system_path.rs b/crates/turborepo-paths/src/absolute_system_path.rs index f535f99534853..0f76c08f800e3 100644 --- a/crates/turborepo-paths/src/absolute_system_path.rs +++ b/crates/turborepo-paths/src/absolute_system_path.rs @@ -103,10 +103,20 @@ impl AbsoluteSystemPath { } } + pub(crate) fn new_unchecked(path: &Path) -> &Self { + unsafe { &*(path as *const Path as *const Self) } + } + pub fn as_path(&self) -> &Path { &self.0 } + pub fn ancestors(&self) -> impl Iterator { + self.0 + .ancestors() + .map(|ancestor| Self::new_unchecked(ancestor)) + } + // intended for joining literals or obviously single-token strings pub fn join_component(&self, segment: &str) -> AbsoluteSystemPathBuf { debug_assert!(!segment.contains(std::path::MAIN_SEPARATOR)); diff --git a/crates/turborepo-paths/src/absolute_system_path_buf.rs b/crates/turborepo-paths/src/absolute_system_path_buf.rs index 7fc57225d4d23..c205a2539fbe4 100644 --- a/crates/turborepo-paths/src/absolute_system_path_buf.rs +++ b/crates/turborepo-paths/src/absolute_system_path_buf.rs @@ -6,6 +6,7 @@ use std::{ path::{Components, Path, PathBuf}, }; +use path_clean::PathClean; use serde::Serialize; use crate::{AbsoluteSystemPath, AnchoredSystemPathBuf, IntoSystem, PathError, RelativeUnixPath}; @@ -65,6 +66,29 @@ impl AbsoluteSystemPathBuf { Ok(AbsoluteSystemPathBuf(system_path)) } + pub fn from_unknown(base: &AbsoluteSystemPath, unknown: impl Into) -> Self { + // we have an absolute system path and an unknown kind of system path. + let unknown: PathBuf = unknown.into(); + if unknown.is_absolute() { + Self(unknown) + } else { + Self(base.as_path().join(unknown).clean()) + } + } + + pub fn from_cwd(unknown: impl Into) -> Result { + let cwd = Self::cwd()?; + Ok(Self::from_unknown(cwd.as_absolute_path(), unknown)) + } + + pub fn cwd() -> Result { + Ok(Self(std::env::current_dir()?)) + } + + pub fn ancestors(&self) -> impl Iterator { + self.as_absolute_path().ancestors() + } + /// Anchors `path` at `self`. /// /// # Arguments