diff --git a/crates/turborepo-lib/src/cli.rs b/crates/turborepo-lib/src/cli.rs index a2489183cbc8c..a996fc0d5590b 100644 --- a/crates/turborepo-lib/src/cli.rs +++ b/crates/turborepo-lib/src/cli.rs @@ -11,6 +11,7 @@ use clap_complete::{generate, Shell}; use dunce::canonicalize as fs_canonicalize; use log::{debug, error}; use serde::Serialize; +use turbopath::AbsoluteSystemPathBuf; use crate::{ commands::{bin, daemon, link, login, logout, unlink, CommandBase}, @@ -488,6 +489,9 @@ pub async fn run(repo_state: Option) -> Result { current_dir()? }; + // 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 clap_args.command.as_ref().unwrap() { diff --git a/crates/turborepo-lib/src/commands/daemon.rs b/crates/turborepo-lib/src/commands/daemon.rs index 0acc40b66adbe..e8cff4d1ffa4a 100644 --- a/crates/turborepo-lib/src/commands/daemon.rs +++ b/crates/turborepo-lib/src/commands/daemon.rs @@ -79,14 +79,12 @@ pub async fn daemon_server(base: &CommandBase, idle_time: &String) -> Result<(), folder.join_relative(logs).join_relative(file) }; - let repo_root = AbsoluteSystemPathBuf::new(base.repo_root.clone()).expect("absolute"); - let timeout = go_parse_duration::parse_duration(idle_time) .map_err(|_| DaemonError::InvalidTimeout(idle_time.to_owned())) .map(|d| Duration::from_nanos(d as u64))?; let server = crate::daemon::DaemonServer::new(base, timeout, log_file)?; - server.serve(repo_root).await; + server.serve().await; Ok(()) } diff --git a/crates/turborepo-lib/src/commands/link.rs b/crates/turborepo-lib/src/commands/link.rs index 80948fb878aad..796cbb69eb8a8 100644 --- a/crates/turborepo-lib/src/commands/link.rs +++ b/crates/turborepo-lib/src/commands/link.rs @@ -16,6 +16,7 @@ use dialoguer::{theme::ColorfulTheme, Confirm}; use dirs_next::home_dir; #[cfg(test)] use rand::Rng; +use turbopath::RelativeSystemPathBuf; use turborepo_api_client::{APIClient, CachingStatus, Space, Team}; #[cfg(not(test))] @@ -166,8 +167,11 @@ pub async fn link( verify_caching_enabled(&api_client, team_id, token, Some(selected_team.clone())) .await?; - fs::create_dir_all(base.repo_root.join(".turbo")) - .context("could not create .turbo directory")?; + fs::create_dir_all( + base.repo_root + .join_relative(RelativeSystemPathBuf::new(".turbo").expect("relative")), + ) + .context("could not create .turbo directory")?; base.repo_config_mut()? .set_team_id(Some(team_id.to_string()))?; @@ -397,7 +401,9 @@ fn enable_caching(url: &str) -> Result<()> { } fn add_turbo_to_gitignore(base: &CommandBase) -> Result<()> { - let gitignore_path = base.repo_root.join(".gitignore"); + let gitignore_path = base + .repo_root + .join_relative(RelativeSystemPathBuf::new(".gitignore").expect("relative")); if !gitignore_path.exists() { let mut gitignore = File::create(gitignore_path)?; @@ -421,7 +427,9 @@ fn add_turbo_to_gitignore(base: &CommandBase) -> Result<()> { } fn add_space_id_to_turbo_json(base: &CommandBase, space_id: &str) -> Result<()> { - let turbo_json_path = base.repo_root.join("turbo.json"); + let turbo_json_path = base + .repo_root + .join_relative(RelativeSystemPathBuf::new("turbo.json").expect("relative")); if !turbo_json_path.exists() { return Err(anyhow!("turbo.json not found.")); @@ -455,6 +463,7 @@ mod test { use tempfile::{NamedTempFile, TempDir}; use tokio::sync::OnceCell; + use turbopath::{AbsoluteSystemPathBuf, RelativeSystemPathBuf}; use vercel_api_mock::start_test_server; use crate::{ @@ -470,6 +479,7 @@ mod test { let user_config_file = NamedTempFile::new().unwrap(); fs::write(user_config_file.path(), r#"{ "token": "hello" }"#).unwrap(); let repo_config_file = NamedTempFile::new().unwrap(); + let repo_config_path = AbsoluteSystemPathBuf::new(repo_config_file.path()).unwrap(); fs::write( repo_config_file.path(), r#"{ "apiurl": "http://localhost:3000" }"#, @@ -489,7 +499,7 @@ mod test { .unwrap(), ), repo_config: OnceCell::from( - RepoConfigLoader::new(repo_config_file.path().to_path_buf()) + RepoConfigLoader::new(repo_config_path) .with_api(Some(format!("http://localhost:{}", port))) .with_login(Some(format!("http://localhost:{}", port))) .load() @@ -519,6 +529,7 @@ mod test { // repo config let repo_config_file = NamedTempFile::new().unwrap(); + let repo_config_path = AbsoluteSystemPathBuf::new(repo_config_file.path()).unwrap(); fs::write( repo_config_file.path(), r#"{ "apiurl": "http://localhost:3000" }"#, @@ -528,7 +539,7 @@ mod test { let port = port_scanner::request_open_port().unwrap(); let handle = tokio::spawn(start_test_server(port)); let mut base = CommandBase { - repo_root: TempDir::new().unwrap().into_path(), + repo_root: AbsoluteSystemPathBuf::new(TempDir::new().unwrap().into_path()).unwrap(), ui: UI::new(false), client_config: OnceCell::from(ClientConfigLoader::new().load().unwrap()), user_config: OnceCell::from( @@ -538,7 +549,7 @@ mod test { .unwrap(), ), repo_config: OnceCell::from( - RepoConfigLoader::new(repo_config_file.path().to_path_buf()) + RepoConfigLoader::new(repo_config_path) .with_api(Some(format!("http://localhost:{}", port))) .with_login(Some(format!("http://localhost:{}", port))) .load() @@ -549,7 +560,10 @@ mod test { }; // turbo config - let turbo_json_file = base.repo_root.join("turbo.json"); + let turbo_json_file = base + .repo_root + .join_relative(RelativeSystemPathBuf::new("turbo.json").expect("relative")); + fs::write( turbo_json_file.as_path(), r#"{ "globalEnv": [], "pipeline": {} }"#, diff --git a/crates/turborepo-lib/src/commands/login.rs b/crates/turborepo-lib/src/commands/login.rs index a512c4670d903..06fdf6bc689e7 100644 --- a/crates/turborepo-lib/src/commands/login.rs +++ b/crates/turborepo-lib/src/commands/login.rs @@ -304,6 +304,7 @@ mod test { use serde::Deserialize; use tempfile::NamedTempFile; use tokio::sync::OnceCell; + use turbopath::AbsoluteSystemPathBuf; use vercel_api_mock::start_test_server; use crate::{ @@ -325,6 +326,7 @@ mod test { let user_config_file = NamedTempFile::new().unwrap(); fs::write(user_config_file.path(), r#"{ "token": "hello" }"#).unwrap(); let repo_config_file = NamedTempFile::new().unwrap(); + let repo_config_path = AbsoluteSystemPathBuf::new(repo_config_file.path()).unwrap(); // Explicitly pass the wrong port to confirm that we're reading it from the // manual override fs::write( @@ -343,7 +345,7 @@ mod test { .unwrap(), ), repo_config: OnceCell::from( - RepoConfigLoader::new(repo_config_file.path().to_path_buf()) + RepoConfigLoader::new(repo_config_path) .with_api(Some(format!("http://localhost:{}", port))) .load() .unwrap(), @@ -376,6 +378,7 @@ mod test { let user_config_file = NamedTempFile::new().unwrap(); fs::write(user_config_file.path(), r#"{ "token": "hello" }"#).unwrap(); let repo_config_file = NamedTempFile::new().unwrap(); + let repo_config_path = AbsoluteSystemPathBuf::new(repo_config_file.path()).unwrap(); // Explicitly pass the wrong port to confirm that we're reading it from the // manual override fs::write( @@ -394,7 +397,7 @@ mod test { .unwrap(), ), repo_config: OnceCell::from( - RepoConfigLoader::new(repo_config_file.path().to_path_buf()) + RepoConfigLoader::new(repo_config_path) .with_api(Some(format!("http://localhost:{}", port))) .load() .unwrap(), diff --git a/crates/turborepo-lib/src/commands/mod.rs b/crates/turborepo-lib/src/commands/mod.rs index db5383c3ee63b..44c89bbc192ee 100644 --- a/crates/turborepo-lib/src/commands/mod.rs +++ b/crates/turborepo-lib/src/commands/mod.rs @@ -1,8 +1,7 @@ -use std::path::PathBuf; - use anyhow::Result; use sha2::{Digest, Sha256}; use tokio::sync::OnceCell; +use turbopath::AbsoluteSystemPathBuf; use turborepo_api_client::APIClient; use crate::{ @@ -22,7 +21,7 @@ pub(crate) mod logout; pub(crate) mod unlink; pub struct CommandBase { - pub repo_root: PathBuf, + pub repo_root: AbsoluteSystemPathBuf, pub ui: UI, user_config: OnceCell, repo_config: OnceCell, @@ -32,7 +31,11 @@ pub struct CommandBase { } impl CommandBase { - pub fn new(args: Args, repo_root: PathBuf, version: &'static str) -> Result { + pub fn new( + args: Args, + repo_root: AbsoluteSystemPathBuf, + version: &'static str, + ) -> Result { Ok(Self { repo_root, ui: args.ui(), @@ -158,19 +161,17 @@ impl CommandBase { #[cfg(test)] mod test { use test_case::test_case; + use turbopath::AbsoluteSystemPathBuf; use crate::get_version; #[test_case("/tmp/turborepo", "6e0cfa616f75a61c"; "basic example")] - #[test_case("", "e3b0c44298fc1c14"; "empty string ok")] fn test_repo_hash(path: &str, expected_hash: &str) { - use std::path::PathBuf; - use super::CommandBase; use crate::Args; let args = Args::default(); - let repo_root = PathBuf::from(path); + let repo_root = AbsoluteSystemPathBuf::new(path).unwrap(); let command_base = CommandBase::new(args, repo_root, get_version()).unwrap(); let hash = command_base.repo_hash(); diff --git a/crates/turborepo-lib/src/commands/unlink.rs b/crates/turborepo-lib/src/commands/unlink.rs index c35ab61ec7049..a5ba8653c64de 100644 --- a/crates/turborepo-lib/src/commands/unlink.rs +++ b/crates/turborepo-lib/src/commands/unlink.rs @@ -1,6 +1,7 @@ use std::fs::File; use anyhow::{Context, Result}; +use turbopath::RelativeSystemPathBuf; use crate::{cli::LinkTarget, commands::CommandBase, config::TurboJson, ui::GREY}; @@ -53,7 +54,9 @@ pub fn unlink(base: &mut CommandBase, target: LinkTarget) -> Result<()> { } fn remove_spaces_from_turbo_json(base: &CommandBase) -> Result { - let turbo_json_path = base.repo_root.join("turbo.json"); + let turbo_json_path = base + .repo_root + .join_relative(RelativeSystemPathBuf::new("turbo.json").expect("relative")); let turbo_json_file = File::open(&turbo_json_path).context("unable to open turbo.json file")?; let mut turbo_json: TurboJson = serde_json::from_reader(turbo_json_file)?; diff --git a/crates/turborepo-lib/src/config/repo.rs b/crates/turborepo-lib/src/config/repo.rs index f960e57dba564..401ee6ea7b774 100644 --- a/crates/turborepo-lib/src/config/repo.rs +++ b/crates/turborepo-lib/src/config/repo.rs @@ -1,11 +1,9 @@ -use std::{ - collections::HashMap, - path::{Path, PathBuf}, -}; +use std::collections::HashMap; use anyhow::Result; use config::Config; use serde::{Deserialize, Serialize}; +use turbopath::{AbsoluteSystemPathBuf, RelativeSystemPathBuf}; use super::{write_to_disk, MappedEnvironment}; @@ -16,7 +14,7 @@ const DEFAULT_LOGIN_URL: &str = "https://vercel.com"; pub struct RepoConfig { disk_config: RepoConfigValue, config: RepoConfigValue, - path: PathBuf, + path: AbsoluteSystemPathBuf, } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Default)] @@ -29,7 +27,7 @@ struct RepoConfigValue { #[derive(Debug, Clone)] pub struct RepoConfigLoader { - path: PathBuf, + path: AbsoluteSystemPathBuf, api: Option, login: Option, teamslug: Option, @@ -69,18 +67,18 @@ impl RepoConfig { } fn write_to_disk(&self) -> Result<()> { - write_to_disk(&self.path, &self.disk_config) + write_to_disk(&self.path.as_path(), &self.disk_config) } } -#[allow(dead_code)] -pub fn get_repo_config_path(repo_root: &Path) -> PathBuf { - repo_root.join(".turbo").join("config.json") +pub fn get_repo_config_path(repo_root: &AbsoluteSystemPathBuf) -> AbsoluteSystemPathBuf { + let config = RelativeSystemPathBuf::new(".turbo/config.json").expect("is relative"); + repo_root.join_relative(config) } impl RepoConfigLoader { #[allow(dead_code)] - pub fn new(path: PathBuf) -> Self { + pub fn new(path: AbsoluteSystemPathBuf) -> Self { Self { path, api: None, @@ -176,9 +174,10 @@ mod test { #[test] fn test_repo_config_with_team_and_api_flags() -> Result<()> { let mut config_file = NamedTempFile::new()?; + let config_path = AbsoluteSystemPathBuf::new(config_file.path())?; writeln!(&mut config_file, "{{\"teamId\": \"123\"}}")?; - let config = RepoConfigLoader::new(config_file.path().to_path_buf()) + let config = RepoConfigLoader::new(config_path) .with_team_slug(Some("my-team-slug".into())) .with_api(Some("http://my-login-url".into())) .load()?; @@ -193,9 +192,9 @@ mod test { #[test] fn test_team_override_clears_id() -> Result<()> { let mut config_file = NamedTempFile::new()?; + let config_path = AbsoluteSystemPathBuf::new(config_file.path())?; writeln!(&mut config_file, "{{\"teamId\": \"123\"}}")?; - let loader = RepoConfigLoader::new(config_file.path().to_path_buf()) - .with_team_slug(Some("foo".into())); + let loader = RepoConfigLoader::new(config_path).with_team_slug(Some("foo".into())); let config = loader.load()?; assert_eq!(config.team_slug(), Some("foo")); @@ -207,10 +206,11 @@ mod test { #[test] fn test_set_team_clears_id() -> Result<()> { let mut config_file = NamedTempFile::new()?; + let config_path = AbsoluteSystemPathBuf::new(config_file.path())?; // We will never pragmatically write the "teamslug" field as camelCase, // but viper is case insensitive and we want to keep this functionality. writeln!(&mut config_file, "{{\"teamSlug\": \"my-team\"}}")?; - let loader = RepoConfigLoader::new(config_file.path().to_path_buf()); + let loader = RepoConfigLoader::new(config_path); let mut config = loader.clone().load()?; config.set_team_id(Some("my-team-id".into()))?; @@ -225,12 +225,13 @@ mod test { #[test] fn test_repo_env_variable() -> Result<()> { let mut config_file = NamedTempFile::new()?; + let config_path = AbsoluteSystemPathBuf::new(config_file.path())?; writeln!(&mut config_file, "{{\"teamslug\": \"other-team\"}}")?; let login_url = "http://my-login-url"; let api_url = "http://my-api"; let team_id = "123"; let team_slug = "my-team"; - let config = RepoConfigLoader::new(config_file.path().to_path_buf()) + let config = RepoConfigLoader::new(config_path) .with_environment({ let mut env = HashMap::new(); env.insert("TURBO_API".into(), api_url.into()); diff --git a/crates/turborepo-lib/src/daemon/server.rs b/crates/turborepo-lib/src/daemon/server.rs index 9861969c71956..76a1e6b3516cb 100644 --- a/crates/turborepo-lib/src/daemon/server.rs +++ b/crates/turborepo-lib/src/daemon/server.rs @@ -47,6 +47,9 @@ use crate::{ }; pub struct DaemonServer { + /// The repo we are watching + repo_root: AbsoluteSystemPathBuf, + /// The root of the daemon files daemon_root: AbsoluteSystemPathBuf, log_file: AbsoluteSystemPathBuf, @@ -77,6 +80,7 @@ impl DaemonServer { log_file: AbsoluteSystemPathBuf, ) -> Result { let daemon_root = base.daemon_file_root(); + let repo_root = base.repo_root.clone(); let watcher = Arc::new(HashGlobWatcher::new( AbsoluteSystemPathBuf::new(base.repo_root.clone()).expect("valid repo root"), @@ -89,6 +93,7 @@ impl DaemonServer { let (send_shutdown, recv_shutdown) = tokio::sync::oneshot::channel::<()>(); Ok(Self { + repo_root, daemon_root, log_file, @@ -112,10 +117,10 @@ impl Drop for DaemonServer { impl DaemonServer { /// Serve the daemon server, while also watching for filesystem changes. - pub async fn serve(mut self, repo_root: AbsoluteSystemPathBuf) -> CloseReason { + pub async fn serve(mut self) -> CloseReason { let stop = StopSource::new(); let watcher = self.watcher.clone(); - let watcher_fut = watcher.watch(repo_root.as_path().to_owned(), stop.token()); + let watcher_fut = watcher.watch(self.repo_root.as_path().to_owned(), stop.token()); let timer = self.timeout.clone(); let timeout_fut = timer.wait(); @@ -309,7 +314,11 @@ mod test { #[tracing_test::traced_test] async fn lifecycle() { let tempdir = tempfile::tempdir().unwrap(); + let path = AbsoluteSystemPathBuf::new(tempdir.path()).unwrap(); + let logs = path.join_relative(RelativeSystemPathBuf::new("turbo.log").unwrap()); + let pid_path = path.join_relative(RelativeSystemPathBuf::new("turbod.pid").unwrap()); + let sock_path = path.join_relative(RelativeSystemPathBuf::new("turbod.sock").unwrap()); tracing::info!("start"); @@ -318,22 +327,19 @@ mod test { Args { ..Default::default() }, - path.as_path().to_path_buf(), + path, "test", ) .unwrap(), Duration::from_secs(60 * 60), - path.clone(), + logs, ) .unwrap(); tracing::info!("server started"); - let pid_path = path.join_relative(RelativeSystemPathBuf::new("turbod.pid").unwrap()); - let sock_path = path.join_relative(RelativeSystemPathBuf::new("turbod.sock").unwrap()); - select! { - _ = daemon.serve(path) => panic!("must not close"), + _ = daemon.serve() => panic!("must not close"), _ = tokio::time::sleep(Duration::from_millis(10)) => (), } @@ -358,7 +364,7 @@ mod test { Args { ..Default::default() }, - path.as_path().to_path_buf(), + path.clone(), "test", ) .unwrap(), @@ -370,15 +376,15 @@ mod test { let pid_path = path.join_relative(RelativeSystemPathBuf::new("turbod.pid").unwrap()); let now = Instant::now(); - let close_reason = daemon.serve(path).await; + let close_reason = daemon.serve().await; assert!( now.elapsed() >= Duration::from_millis(5), "must wait at least 5ms" ); assert_matches::assert_matches!( - super::CloseReason::Timeout, close_reason, + super::CloseReason::Timeout, "must close due to timeout" ); assert!(!pid_path.exists(), "pid file must be deleted");