diff --git a/crates/fbuild-cli/src/cli/args.rs b/crates/fbuild-cli/src/cli/args.rs index cf975e8f7..b0ae08555 100644 --- a/crates/fbuild-cli/src/cli/args.rs +++ b/crates/fbuild-cli/src/cli/args.rs @@ -1066,9 +1066,17 @@ pub fn resolve_project_dir( subcommand_dir: Option, top_level_dir: &Option, ) -> String { - subcommand_dir + let raw = subcommand_dir .or_else(|| top_level_dir.clone()) - .unwrap_or_else(|| ".".to_string()) + .unwrap_or_else(|| ".".to_string()); + // Send the daemon an absolute path. A relative one is read against the + // daemon's working directory, and every path derived from it stays + // relative while the compiler runs from the project dir -- which is how + // `fbuild ci/kitchensink build` lost its libraries' include paths + // (FastLED/fbuild#1441). Lexical only: symlinks are not resolved. + std::path::absolute(&raw) + .map(|p| p.to_string_lossy().into_owned()) + .unwrap_or(raw) } /// Known subcommand names for arg rewriting. diff --git a/crates/fbuild-daemon/src/handlers/operations/build.rs b/crates/fbuild-daemon/src/handlers/operations/build.rs index 4d9029a1d..6838065cf 100644 --- a/crates/fbuild-daemon/src/handlers/operations/build.rs +++ b/crates/fbuild-daemon/src/handlers/operations/build.rs @@ -2,6 +2,7 @@ use super::common::{ OperationGuard, export_artifacts_bundle, resolve_build_dir, resolve_client_path, + resolve_request_project_dir, }; use crate::context::DaemonContext; use crate::models::{BuildRequest, OperationResponse}; @@ -9,7 +10,6 @@ use axum::Json; use axum::extract::State; use axum::http::StatusCode; use fbuild_core::channel::{UnboundedReceiver, UnboundedSender, unbounded}; -use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use tokio::sync::Notify; @@ -146,7 +146,7 @@ pub async fn build( let request_id = req .request_id .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); - let project_dir = PathBuf::from(&req.project_dir); + let project_dir = resolve_request_project_dir(&req.project_dir, req.caller_cwd.as_deref()); let stream = req.stream; if !project_dir.exists() { diff --git a/crates/fbuild-daemon/src/handlers/operations/common.rs b/crates/fbuild-daemon/src/handlers/operations/common.rs index 1106eb169..01365b582 100644 --- a/crates/fbuild-daemon/src/handlers/operations/common.rs +++ b/crates/fbuild-daemon/src/handlers/operations/common.rs @@ -335,6 +335,26 @@ pub(crate) fn parse_deploy_route( } } +/// A request's `project_dir` as an absolute path. +/// +/// A relative project dir must be read against the *caller's* working +/// directory, not the daemon's. Left relative, every path derived from it -- +/// the build dir, downloaded libraries, their `-I` flags -- stays relative, +/// while the compiler runs from the absolute project dir, so those flags point +/// nowhere (FastLED/fbuild#1441: `fbuild ci/kitchensink build` could not find +/// a registry library's headers). Lexical only: symlinks are not resolved. +pub(crate) fn resolve_request_project_dir(raw: &str, caller_cwd: Option<&str>) -> PathBuf { + let path = PathBuf::from(raw); + if path.is_absolute() { + return path; + } + let joined = match caller_cwd { + Some(cwd) => PathBuf::from(cwd).join(path), + None => path, + }; + std::path::absolute(&joined).unwrap_or(joined) +} + pub(crate) fn resolve_client_path( raw: &str, caller_cwd: Option<&str>, diff --git a/crates/fbuild-daemon/src/handlers/operations/deploy.rs b/crates/fbuild-daemon/src/handlers/operations/deploy.rs index 0ce7d6463..34298416f 100644 --- a/crates/fbuild-daemon/src/handlers/operations/deploy.rs +++ b/crates/fbuild-daemon/src/handlers/operations/deploy.rs @@ -3,7 +3,7 @@ use super::common::{ DeployRoute, EmulatorKind, OperationGuard, compute_esp32_image_hash, export_artifacts_bundle, infer_default_emulator_kind, parse_deploy_route, qemu_extra_build_flags, resolve_build_dir, - resolve_client_path, trust_device_hash_enabled, + resolve_client_path, resolve_request_project_dir, trust_device_hash_enabled, }; use super::deploy_port::{append_warning_to_stderr, choose_deploy_port}; use super::monitor::{MonitorOutcome, run_monitor_loop}; @@ -73,7 +73,7 @@ pub async fn deploy( .request_id .clone() .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); - let project_dir = PathBuf::from(&req.project_dir); + let project_dir = resolve_request_project_dir(&req.project_dir, req.caller_cwd.as_deref()); if !project_dir.exists() { return ( diff --git a/crates/fbuild-daemon/src/handlers/operations/install_deps.rs b/crates/fbuild-daemon/src/handlers/operations/install_deps.rs index 319839c29..486adbad7 100644 --- a/crates/fbuild-daemon/src/handlers/operations/install_deps.rs +++ b/crates/fbuild-daemon/src/handlers/operations/install_deps.rs @@ -1,13 +1,12 @@ //! `POST /api/install-deps` — fetch toolchains, frameworks, and libraries //! without building. -use super::common::OperationGuard; +use super::common::{OperationGuard, resolve_request_project_dir}; use crate::context::DaemonContext; use crate::models::{InstallDepsRequest, OperationResponse}; use axum::Json; use axum::extract::State; use axum::http::StatusCode; -use std::path::PathBuf; use std::sync::Arc; /// POST /api/install-deps @@ -21,7 +20,7 @@ pub async fn install_deps( let request_id = req .request_id .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); - let project_dir = PathBuf::from(&req.project_dir); + let project_dir = resolve_request_project_dir(&req.project_dir, req.caller_cwd.as_deref()); if !project_dir.exists() { return ( diff --git a/crates/fbuild-daemon/src/handlers/operations/tests.rs b/crates/fbuild-daemon/src/handlers/operations/tests.rs index 2e2e1a35e..a6137054f 100644 --- a/crates/fbuild-daemon/src/handlers/operations/tests.rs +++ b/crates/fbuild-daemon/src/handlers/operations/tests.rs @@ -194,3 +194,44 @@ mod image_hash_memo_tests { ); } } + +mod request_project_dir_tests { + //! A request's project dir must come out absolute, read against the + //! caller's working directory (FastLED/fbuild#1441). + use super::super::common::resolve_request_project_dir; + use std::path::Path; + + #[test] + fn relative_dir_is_joined_onto_caller_cwd() { + let cwd = std::env::temp_dir().join("caller"); + let resolved = resolve_request_project_dir("ci/kitchensink", Some(cwd.to_str().unwrap())); + assert!(resolved.is_absolute()); + assert_eq!(resolved, cwd.join("ci").join("kitchensink")); + } + + #[test] + fn absolute_dir_is_kept_as_is() { + let abs = std::env::temp_dir().join("proj"); + let resolved = resolve_request_project_dir(abs.to_str().unwrap(), Some("/elsewhere")); + assert_eq!(resolved, abs); + } + + #[test] + fn relative_dir_without_caller_cwd_is_still_absolute() { + let resolved = resolve_request_project_dir("proj", None); + assert!(resolved.is_absolute()); + assert!(resolved.ends_with(Path::new("proj"))); + } + + #[test] + fn dot_resolves_to_the_caller_cwd_itself() { + let cwd = std::env::temp_dir().join("caller"); + let resolved = resolve_request_project_dir(".", Some(cwd.to_str().unwrap())); + assert!(resolved.is_absolute()); + // `std::path::absolute` keeps a trailing `.` component lexically on + // some hosts; compare by components that matter. + let components: Vec<_> = resolved.components().collect(); + let expected: Vec<_> = cwd.components().collect(); + assert_eq!(components, expected); + } +}