Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions crates/fbuild-build-arm/src/generic_arm/arm_linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use fbuild_core::subprocess::run_command;
use fbuild_core::{BuildProfile, Result, SizeInfo};

use super::mcu_config::ArmMcuConfig;
use crate::linker::{LinkExtraArgs, Linker};
use crate::linker::{LinkExtraArgs, Linker, link_cwd_for};

/// Generic ARM linker using arm-none-eabi-gcc (link driver), ar, objcopy, size.
pub struct ArmLinker {
Expand Down Expand Up @@ -151,6 +151,20 @@ impl Linker for ArmLinker {
tracing::debug!(target: "fbuild_build::linker::generic_arm", "link: {}", args.join(" "));
}

// Run the link in the firmware's own output directory rather than
// inheriting the daemon's cwd (typically the user's project root):
// arm-none-eabi-gcc hands off to collect2 / lto-wrapper, which write
// scratch files relative to the process cwd. See FastLED/fbuild#1269
// (and #1268, which fixed the same bug for teensy).
let link_cwd = link_cwd_for(
output_dir,
objects
.iter()
.chain(archives.iter())
.chain(self.lib_search_dirs.iter())
.chain(std::iter::once(&self.linker_script_path)),
);
Comment on lines +159 to +166

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Include every path-bearing linker operand in the cwd decision.

Each link command adds raw configuration or extra operands before this check. A raw -Lrelative, -Trelative, or direct relative archive is not included in link_cwd_for. If all tracked paths are absolute, the new cwd changes how that raw operand resolves and can break the link.

Represent path-bearing raw operands as structured paths when the command is built, then include them in cwd_sensitive. Add a failing regression test for a relative raw linker path before the implementation change.

  • crates/fbuild-build-arm/src/generic_arm/arm_linker.rs#L159-L166: include raw linker flags, profile flags, and extra operands that resolve relative to cwd.
  • crates/fbuild-build-arm/src/nrf52/nrf52_linker.rs#L132-L139: include raw linker flags, profile flags, and extra operands that resolve relative to cwd.
  • crates/fbuild-build-arm/src/renesas/renesas_linker.rs#L139-L145: include raw linker flags, profile flags, and extra operands that resolve relative to cwd.
  • crates/fbuild-build-arm/src/sam/sam_linker.rs#L153-L168: include raw linker flags and extra operands in addition to the existing structured paths.
  • crates/fbuild-build-arm/src/silabs/silabs_linker.rs#L140-L147: include raw linker flags, profile flags, and extra operands that resolve relative to cwd.
  • crates/fbuild-build-arm/src/teensy/teensy_linker.rs#L162-L168: include raw linker flags, profile flags, and extra operands that resolve relative to cwd.

As per coding guidelines, “Follow TDD: write failing tests first, implement the minimum passing behavior, then refactor.”

📍 Affects 6 files
  • crates/fbuild-build-arm/src/generic_arm/arm_linker.rs#L159-L166 (this comment)
  • crates/fbuild-build-arm/src/nrf52/nrf52_linker.rs#L132-L139
  • crates/fbuild-build-arm/src/renesas/renesas_linker.rs#L139-L145
  • crates/fbuild-build-arm/src/sam/sam_linker.rs#L153-L168
  • crates/fbuild-build-arm/src/silabs/silabs_linker.rs#L140-L147
  • crates/fbuild-build-arm/src/teensy/teensy_linker.rs#L162-L168
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fbuild-build-arm/src/generic_arm/arm_linker.rs` around lines 159 -
166, link_cwd_for must consider every cwd-sensitive linker operand, not only
existing structured paths. First add a failing regression test for a relative
raw linker path, then update the command-building flows at
crates/fbuild-build-arm/src/generic_arm/arm_linker.rs:159-166,
crates/fbuild-build-arm/src/nrf52/nrf52_linker.rs:132-139,
crates/fbuild-build-arm/src/renesas/renesas_linker.rs:139-145,
crates/fbuild-build-arm/src/sam/sam_linker.rs:153-168,
crates/fbuild-build-arm/src/silabs/silabs_linker.rs:140-147, and
crates/fbuild-build-arm/src/teensy/teensy_linker.rs:162-168 to represent raw
linker flags, profile flags, and extra operands (where applicable) as structured
paths and include them in cwd_sensitive before calling link_cwd_for. Preserve
the existing structured-path handling and make the minimum implementation pass
the regression test.

Source: Coding guidelines


// GCC LTO temp dir for MSYS-safe paths — see FastLED/fbuild#261.
let lto_env = fbuild_core::subprocess::link_env_for_build(output_dir)?;
let env_slice: Vec<(&str, &str)> = lto_env
Expand Down Expand Up @@ -183,14 +197,14 @@ impl Linker for ArmLinker {
let rsp_arg = format!("@{}", rsp_path.display());
run_command(
&[args[0].as_str(), &rsp_arg],
None,
link_cwd,
Some(&env_slice),
link_timeout,
)
.await?
} else {
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
run_command(&args_ref, None, Some(&env_slice), link_timeout).await?
run_command(&args_ref, link_cwd, Some(&env_slice), link_timeout).await?
};

if !result.success() {
Expand Down
18 changes: 16 additions & 2 deletions crates/fbuild-build-arm/src/nrf52/nrf52_linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use fbuild_core::subprocess::run_command;
use fbuild_core::{BuildProfile, Result, SizeInfo};

use super::mcu_config::Nrf52McuConfig;
use crate::linker::{LinkExtraArgs, Linker};
use crate::linker::{LinkExtraArgs, Linker, link_cwd_for};

/// NRF52-specific linker using arm-none-eabi-gcc (link driver), ar, objcopy, size.
pub struct Nrf52Linker {
Expand Down Expand Up @@ -124,11 +124,25 @@ impl Linker for Nrf52Linker {
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();

// Run the link in the firmware's own output directory rather than
// inheriting the daemon's cwd (typically the user's project root):
// arm-none-eabi-gcc hands off to collect2 / lto-wrapper, which write
// scratch files relative to the process cwd. See FastLED/fbuild#1269
// (and #1268, which fixed the same bug for teensy).
let link_cwd = link_cwd_for(
output_dir,
objects
.iter()
.chain(archives.iter())
.chain(self.linker_search_dirs.iter())
.chain(std::iter::once(&self.linker_script_path)),
);

let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
// FastLED/fbuild#809: bound the link step at 3 min.
let result = run_command(
&args_ref,
None,
link_cwd,
Some(&env_slice),
Some(std::time::Duration::from_secs(180)),
)
Expand Down
19 changes: 17 additions & 2 deletions crates/fbuild-build-arm/src/renesas/renesas_linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use fbuild_core::subprocess::run_command;
use fbuild_core::{BuildProfile, Result, SizeInfo};

use super::mcu_config::RenesasMcuConfig;
use crate::linker::{LinkExtraArgs, Linker};
use crate::linker::{LinkExtraArgs, Linker, link_cwd_for};

/// Renesas-specific linker using arm-none-eabi-gcc (link driver), ar, objcopy, size.
pub struct RenesasLinker {
Expand Down Expand Up @@ -129,11 +129,26 @@ impl Linker for RenesasLinker {
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();

// Run the link in the firmware's own output directory rather than
// inheriting the daemon's cwd (typically the user's project root):
// arm-none-eabi-gcc hands off to collect2 / lto-wrapper, which write
// scratch files relative to the process cwd. See FastLED/fbuild#1269
// (and #1268, which fixed the same bug for teensy). The `-L` variant
// dir and the `libfsp.a` path are both derived from
// `linker_script_path`, so checking it covers them too.
let link_cwd = link_cwd_for(
output_dir,
objects
.iter()
.chain(archives.iter())
.chain(std::iter::once(&self.linker_script_path)),
);

let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
// FastLED/fbuild#809: bound the link step at 3 min.
let result = run_command(
&args_ref,
None,
link_cwd,
Some(&env_slice),
Some(std::time::Duration::from_secs(180)),
)
Expand Down
26 changes: 24 additions & 2 deletions crates/fbuild-build-arm/src/sam/sam_linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use fbuild_core::subprocess::run_command;
use fbuild_core::{BuildProfile, Result, SizeInfo};

use super::mcu_config::SamMcuConfig;
use crate::linker::{LinkExtraArgs, Linker};
use crate::linker::{LinkExtraArgs, Linker, link_cwd_for};

/// SAM-specific linker using arm-none-eabi-gcc (link driver), ar, objcopy, size.
pub struct SamLinker {
Expand Down Expand Up @@ -145,11 +145,33 @@ impl Linker for SamLinker {
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();

// Run the link in the firmware's own output directory rather than
// inheriting the daemon's cwd (typically the user's project root):
// arm-none-eabi-gcc hands off to collect2 / lto-wrapper, which write
// scratch files relative to the process cwd. See FastLED/fbuild#1269
// (and #1268, which fixed the same bug for teensy).
let mut cwd_sensitive: Vec<&Path> = vec![&self.linker_script_path];
cwd_sensitive.extend(objects.iter().map(|p| p.as_path()));
cwd_sensitive.extend(archives.iter().map(|p| p.as_path()));
cwd_sensitive.extend(self.extra_lib_dirs.iter().map(|p| p.as_path()));
// `extra_libs` entries that name a file rather than a `-l<name>` are
// passed through verbatim, so they are cwd-sensitive too.
cwd_sensitive.extend(
self.extra_libs
.iter()
.filter(|lib| {
!lib.starts_with("-l")
&& (lib.contains(std::path::MAIN_SEPARATOR) || lib.contains('/'))
})
.map(Path::new),
);
let link_cwd = link_cwd_for(output_dir, &cwd_sensitive);

let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
// FastLED/fbuild#809: bound the link step at 3 min.
let result = run_command(
&args_ref,
None,
link_cwd,
Some(&env_slice),
Some(std::time::Duration::from_secs(180)),
)
Expand Down
18 changes: 16 additions & 2 deletions crates/fbuild-build-arm/src/silabs/silabs_linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use fbuild_core::subprocess::run_command;
use fbuild_core::{BuildProfile, Result, SizeInfo};

use super::mcu_config::SilabsMcuConfig;
use crate::linker::{LinkExtraArgs, Linker};
use crate::linker::{LinkExtraArgs, Linker, link_cwd_for};

/// Silicon Labs-specific linker using arm-none-eabi-gcc (link driver), ar, objcopy, size.
pub struct SilabsLinker {
Expand Down Expand Up @@ -132,11 +132,25 @@ impl Linker for SilabsLinker {
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();

// Run the link in the firmware's own output directory rather than
// inheriting the daemon's cwd (typically the user's project root):
// arm-none-eabi-gcc hands off to collect2 / lto-wrapper, which write
// scratch files relative to the process cwd. See FastLED/fbuild#1269
// (and #1268, which fixed the same bug for teensy).
let mut cwd_sensitive: Vec<&Path> = vec![&self.linker_script_path];
cwd_sensitive.extend(objects.iter().map(|p| p.as_path()));
cwd_sensitive.extend(archives.iter().map(|p| p.as_path()));
cwd_sensitive.extend(self.precompiled_libs.iter().map(|p| p.as_path()));
if let Some(gsdk) = &self.precompiled_gsdk {
cwd_sensitive.push(gsdk);
}
let link_cwd = link_cwd_for(output_dir, &cwd_sensitive);

let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
// FastLED/fbuild#809: bound the link step at 3 min.
let result = run_command(
&args_ref,
None,
link_cwd,
Some(&env_slice),
Some(std::time::Duration::from_secs(180)),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,15 @@ async fn preprocess_linker_script(
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
// FastLED/fbuild#809: linker-script preprocessing is a trivial
// g++ -E invocation; bound to 30s.
//
// Deliberately keeps the inherited cwd (FastLED/fbuild#1269): this is not
// a link. `-E -P` stops after preprocessing, so no `collect2` /
// `lto-wrapper` is spawned and nothing writes cwd-relative scratch files —
// the stray `-r` class of junk cannot originate here. Both the input
// (`variant_dir`) and the output (`build_dir`) are already explicit paths,
// so the cwd does not affect where anything lands. The `None` env is
// likewise correct: `link_env_for_build` only redirects GCC's *LTO* temp
// dir, which a preprocess-only run never uses.
let result = fbuild_core::subprocess::run_command(
&args_ref,
None,
Expand Down
63 changes: 28 additions & 35 deletions crates/fbuild-build-arm/src/teensy/teensy_linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use fbuild_core::subprocess::run_command;
use fbuild_core::{BuildProfile, Result, SizeInfo};

use super::mcu_config::TeensyMcuConfig;
use crate::linker::{LinkExtraArgs, Linker, LinkerScripts};
use crate::linker::{LinkExtraArgs, Linker, LinkerScripts, link_cwd_for};

/// Teensy-specific linker using arm-none-eabi-gcc (link driver), ar, objcopy, size.
pub struct TeensyLinker {
Expand Down Expand Up @@ -120,21 +120,6 @@ impl TeensyLinker {
}
}

/// Working directory to run the teensy link in.
///
/// Returns the output directory when it is absolute, so collect2 /
/// lto-wrapper scratch files land in the gitignored build tree instead of
/// the daemon's inherited cwd (the user's project root). Returns `None` for
/// a relative `output_dir`, where changing cwd would also relocate the
/// relative `-o` path — preserving the historical behaviour for that case.
fn link_cwd_for(output_dir: &Path) -> Option<&Path> {
if output_dir.is_absolute() {
Some(output_dir)
} else {
None
}
}

#[async_trait::async_trait]
impl Linker for TeensyLinker {
async fn archive(&self, objects: &[PathBuf], output: &Path) -> Result<()> {
Expand Down Expand Up @@ -168,12 +153,19 @@ impl Linker for TeensyLinker {
// in the build dir). This completes FastLED/fbuild#261, which
// redirected only the env-based LTO temp dir and left cwd inherited.
//
// Safe because every link argument is absolute: `-o` is
// `output_dir.join(...)`, objects/archives arrive absolute, and
// `-T<script>` resolves through absolute `-L` search paths. The
// is_absolute guard keeps the old behaviour for any caller that
// passes a relative output_dir, where moving cwd would relocate `-o`.
let link_cwd = link_cwd_for(output_dir);
// Safe only when every cwd-sensitive link argument is absolute: `-o`
// is `output_dir.join(...)`, and `-T<script>` is a bare *name*
// resolved through the `-L` search paths, so the guard checks the
// objects, the archives and those search dirs. `link_cwd_for` keeps
// the old behaviour if any of them is relative, where moving cwd
// would relocate `-o` or re-resolve an input.
let link_cwd = link_cwd_for(
output_dir,
objects
.iter()
.chain(archives.iter())
.chain(self.linker_scripts.search_dirs.iter()),
);

// Redirect GCC LTO temp files into a forward-slashed, fbuild-owned
// dir under the build dir so MSYS `mv` doesn't collapse backslashes
Expand Down Expand Up @@ -277,29 +269,30 @@ mod tests {
/// The link must not run in the daemon's inherited cwd: collect2 /
/// lto-wrapper write scratch files relative to it, which put a stray
/// 0-byte `-r` in the user's repo root on every clean build.
/// FastLED/fbuild#1267, FastLED/FastLED#3867.
/// FastLED/fbuild#1267, FastLED/FastLED#3867. The shared helper itself is
/// unit-tested in `fbuild-build-engine`; this pins the teensy call shape —
/// the `-T` script is a bare name, so it is the `-L` search dirs that must
/// be absolute.
#[test]
fn link_runs_in_absolute_output_dir_not_inherited_cwd() {
let abs = if cfg!(windows) {
PathBuf::from("C:\\proj\\.fbuild\\build\\release")
let (out, core_dir) = if cfg!(windows) {
("C:\\proj\\.fbuild\\build\\release", "C:\\pkgs\\teensy4")
} else {
PathBuf::from("/proj/.fbuild/build/release")
("/proj/.fbuild/build/release", "/pkgs/teensy4")
};
let scripts = LinkerScripts::single(PathBuf::from(core_dir), "imxrt1062_t41.ld");
let objects = [PathBuf::from(out).join("sketch.o")];
assert_eq!(
link_cwd_for(&abs),
Some(abs.as_path()),
link_cwd_for(
Path::new(out),
objects.iter().chain(scripts.search_dirs.iter())
),
Some(Path::new(out)),
"absolute output dir must become the link cwd so linker scratch \
files stay in the build tree"
);
}

/// A relative output dir keeps the historical inherited-cwd behaviour:
/// moving cwd there would also relocate the relative `-o` path.
#[test]
fn link_cwd_is_unset_for_relative_output_dir() {
assert_eq!(link_cwd_for(Path::new("build/release")), None);
}

#[test]
fn test_teensy_linker_creation() {
let linker = TeensyLinker::new(
Expand Down
Loading
Loading