Skip to content

Commit 0777baf

Browse files
zackeesclaude
andauthored
refactor(paths) + fix(symbols): BuildLayout single-source + PT_LOAD bloat filter (#455)
* refactor(paths): make BuildLayout the single source of truth for the build dir Closes the FastLED-shaped path bug #432: a project staged at `<repo>/.build/pio/<board>/` and built with `env == board` was yielding `.build/pio/<board>/.fbuild/build/<board>/release/firmware.hex` — the board name appearing twice and the path deep enough to threaten Windows' 260-char MAX_PATH limit. Two related issues fixed together: 1. **`BuildParams.build_dir` was dead code.** The daemon HTTP handlers carefully computed it via `get_project_build_root(&project_dir)` and passed it through, but `pipeline/context.rs` ignored that field and re-derived the build dir via `Cache::new(project_dir).get_build_dir (env, profile)`. Zero `params.build_dir` references in `crates/fbuild-build/src/**` confirmed the field was load-bearing only in name. HTTP callers had no way to actually override the build dir. 2. **No collapse rule for the `<env>` segment.** `compile_many.rs` separately hard-coded `.fbuild/build/<env>/<profile>` instead of going through `fbuild-paths`. Different code paths drifted. Changes: - `fbuild-paths::BuildLayout`: single resolver with precedence `override_root` > `FBUILD_BUILD_DIR` > `<project>/.fbuild/build`, plus the `<env>/<profile>` append. Drops the `<env>` segment when `flatten_env` is set or auto-detects when `project_dir.file_name() == env_name` — the FastLED `.build/pio/<board>/` case is now fixed with zero consumer-side change. - `pipeline/context.rs:120` now reads `params.build_dir` directly instead of re-deriving via `Cache`. The build_dir field is finally load-bearing. - `Cache::get_build_dir`, `compile_many::project_build_dir`, and `fbuild-paths::find_firmware` route through `BuildLayout` so layout decisions live in exactly one place. - HTTP request models (`BuildRequest`, `DeployRequest`, `TestEmuRequest`) gained `build_dir_override` + `flatten_env`. Embedders (FastLED) can opt in over HTTP. - Per-platform integration tests migrated from `tmp.path().join(".fbuild/build")` to the explicit `<env>/<profile>` shape that matches the new contract. Regression tests added: `find_firmware_in_collapsed_layout_when_basename_matches_env` in `fbuild-paths` and `project_build_dir_collapses_when_sketch_basename_matches_env` in `fbuild-build`. Workspace verification: - `soldr cargo clippy --workspace --all-targets -- -D warnings` clean. - `soldr cargo test --workspace` — 52 test crates, zero failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(symbols): drop symbols outside PT_LOAD regions from bloat report `fbuild bloat`/`fbuild symbols` was reporting nm's full symbol-table view, including linker-script boundary markers whose nm-computed "size" is the address gap to the next symbol — which can be multiple gigabytes. On an nrf52840 firmware with a real `.text` of 32,740 B, the bloat report claimed flash = 8,052,240,525 B (~8 GB), with `__flash_arduino_end` (4 GB) and `__StackTop` (3.5 GB) topping the list. Those are linker markers, not bytes in the final binary. Fix: - New `fbuild_core::symbol_analysis::LoadedRegion` representing a PT_LOAD program-header range. `FineGrainedSymbolMap::retain_loaded_symbols` filters the symbol list to those whose `[addr, addr + size)` fits strictly inside some loaded region, then recomputes totals. - Strict-lower-bound containment (`addr < region.end`) so size-0 boundary symbols at the end-of-segment (the `__StackTop` case) are excluded. - `fbuild_build::symbol_analyzer::read_pt_load_regions` parses PT_LOAD segments from the ELF via the `object` crate (already a dev-dep; promoted to a regular dep). Supports ELF32 + ELF64. Failures degrade to a `tracing::warn!` plus unfiltered output rather than erroring — the bloat report should still surface useful data on a corrupt or non-ELF input. - `analyze_elf` calls `retain_loaded_symbols` after building the map, so every consumer of `fbuild symbols` / `fbuild bloat` benefits automatically. After the fix on the same nrf52840 ELF: - 520 → 519 symbols - total_flash 8 GB → 33,083 B (within 1 % of readelf's `.text` = 32,740 B; small overshoot from weak symbols counted twice by nm). - `__flash_arduino_end` and `__StackTop` no longer appear. - `__HeapBase` correctly remains under RAM (its address is inside the RAM PT_LOAD range; the 227 KB number reflects the .heap region's NOBITS allocation). Unit tests: - `loaded_region_strict_containment`: covers strict lower bound, end-aligned upper bound, overflow guard. - `retain_loaded_symbols_drops_boundary_markers`: pins the fix against synthetic `__StackTop` / `__flash_arduino_end` / oversized inputs. - `retain_loaded_symbols_no_op_when_regions_empty`: defensive case for PT_LOAD probe failure (don't empty the map). Workspace verification: - `soldr cargo clippy --workspace --all-targets -- -D warnings` clean. - `soldr cargo test --workspace` — 52 test crates, zero failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(deploy): inline override-root build to keep deploy.rs under 1000 LOC The BuildLayout migration in this branch pushed the file from 998 → 1002 LOC, tripping the workspace's `Reject .rs files over 1000 LOC` gate. Compress the override-root resolution into a single chained expression — same behavior, three fewer lines. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(daemon): extract resolve_build_dir helper so deploy.rs stays under 1000 LOC The previous BuildLayout migration duplicated the `override > FBUILD_BUILD_DIR > default + flatten_env` resolution in deploy.rs and build.rs. rustfmt's preferred form pushed deploy.rs to 1002 LOC, tripping the workspace's per-file gate. Lift the resolution into `common::resolve_build_dir` so: - deploy.rs drops back to 998 LOC. - Both HTTP handlers share one implementation of the precedence rules, so future changes can't drift between them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 24b7de4 commit 0777baf

27 files changed

Lines changed: 1083 additions & 69 deletions

crates/fbuild-build/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ walkdir = { workspace = true }
2424
sha2 = { workspace = true }
2525
blake3 = { workspace = true }
2626
tempfile = { workspace = true }
27+
object = { workspace = true }
2728

2829
[dev-dependencies]
2930
filetime = { workspace = true }
3031
fbuild-test-support = { path = "../fbuild-test-support" }
3132
tar = { workspace = true }
32-
object = { workspace = true }

crates/fbuild-build/src/compile_many.rs

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -87,19 +87,14 @@ pub fn default_sketch_jobs() -> usize {
8787

8888
/// Resolve the on-disk build root the orchestrator uses for a given sketch.
8989
///
90-
/// Matches the convention used by every per-platform orchestrator:
91-
/// `<sketch>/.fbuild/build/<env>/<profile>/`. Centralized here so the
92-
/// stage-1→stage-2 cache-seeding code can address that path without
93-
/// re-deriving the same string in three places. See FastLED/fbuild#335.
90+
/// Routes through [`fbuild_paths::BuildLayout`] so layout decisions
91+
/// (env-segment auto-collapse, `FBUILD_BUILD_DIR` override) match
92+
/// what the daemon and the per-platform orchestrators use. Centralized
93+
/// here so the stage-1→stage-2 cache-seeding code can address that path
94+
/// without re-deriving the same string in three places. See
95+
/// FastLED/fbuild#335.
9496
pub fn project_build_dir(sketch: &Path, env: &str, profile: BuildProfile) -> PathBuf {
95-
sketch
96-
.join(".fbuild")
97-
.join("build")
98-
.join(env)
99-
.join(match profile {
100-
BuildProfile::Release => "release",
101-
BuildProfile::Quick => "quick",
102-
})
97+
fbuild_paths::BuildLayout::new(sketch.to_path_buf(), env.to_string(), profile).resolve()
10398
}
10499

105100
/// Seed a stage-2 project's framework `core/` from stage 1's, so the
@@ -833,6 +828,24 @@ mod tests {
833828
assert!(q.ends_with("sketch/.fbuild/build/esp32s3/quick"));
834829
}
835830

831+
/// FastLED stages each board's project at
832+
/// `<repo>/.build/pio/<board>/` and asks fbuild to build with
833+
/// `env == board`. `project_build_dir` must collapse the duplicate
834+
/// `<board>` segment (via `BuildLayout`'s auto-detect rule) so the
835+
/// resulting tree fits under Windows' 260-char `MAX_PATH` limit and
836+
/// matches what `find_firmware` looks for. See FastLED/fbuild#432.
837+
#[test]
838+
fn project_build_dir_collapses_when_sketch_basename_matches_env() {
839+
let sketch = Path::new("/repo/.build/pio/teensy40");
840+
let p = project_build_dir(sketch, "teensy40", BuildProfile::Release);
841+
let s = p.to_string_lossy().to_string();
842+
assert!(
843+
!s.contains("build/teensy40/release") && !s.contains("build\\teensy40\\release"),
844+
"stage-2 build dir kept duplicated env segment: {s}"
845+
);
846+
assert!(p.ends_with("release"));
847+
}
848+
836849
#[test]
837850
fn resolve_env_picks_literal_env_name() {
838851
let tmp = tempfile::tempdir().unwrap();

crates/fbuild-build/src/pipeline/context.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,16 +110,22 @@ impl BuildContext {
110110
}
111111

112112
// 4. Setup build directories
113+
//
114+
// `params.build_dir` is the authoritative env-rooted build dir
115+
// (caller resolves it via `fbuild_paths::BuildLayout`). We never
116+
// re-derive it from `project_dir` here, so callers can override
117+
// the layout (e.g. flatten the env segment when their project
118+
// dir is already named after the env — the FastLED
119+
// `.build/pio/<board>/` case). See FastLED/fbuild#432.
113120
let t0 = std::time::Instant::now();
114-
let cache = fbuild_packages::Cache::new(project_dir);
115-
if params.clean {
116-
cache.clean_build(env_name, params.profile)?;
121+
let build_dir = params.build_dir.clone();
122+
if params.clean && build_dir.exists() {
123+
std::fs::remove_dir_all(&build_dir)?;
117124
}
118-
cache.ensure_build_directories(env_name, params.profile)?;
119-
120-
let build_dir = cache.get_build_dir(env_name, params.profile);
121-
let core_build_dir = cache.get_core_build_dir(env_name, params.profile);
122-
let src_build_dir = cache.get_src_build_dir(env_name, params.profile);
125+
let core_build_dir = build_dir.join("core");
126+
let src_build_dir = build_dir.join("src");
127+
std::fs::create_dir_all(&core_build_dir)?;
128+
std::fs::create_dir_all(&src_build_dir)?;
123129
if let Some(p) = perf.as_mut() {
124130
p.record("build-dirs", t0.elapsed());
125131
}

crates/fbuild-build/src/symbol_analyzer.rs

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,93 @@ use std::collections::BTreeMap;
1515
use fbuild_core::subprocess::run_command_with_stdin;
1616
use fbuild_core::symbol_analysis::{
1717
build_fine_grained_map_with_synth, collect_map_derived_owners, parse_linker_map,
18-
parse_nm_output, FineGrainedSymbolMap,
18+
parse_nm_output, FineGrainedSymbolMap, LoadedRegion,
1919
};
2020
use fbuild_core::{FbuildError, Result};
2121

22+
/// Read the `PT_LOAD` program-header ranges from an ELF. These are the
23+
/// regions that actually get programmed into the device's flash/RAM
24+
/// image at boot — every other byte in the ELF (debug info,
25+
/// `.ARM.attributes`, `.comment`, symbol tables) lives only in the
26+
/// host-side file and never reaches the binary.
27+
///
28+
/// Used by [`analyze_elf`] to drop linker-script boundary symbols
29+
/// (`__StackTop`, `__flash_arduino_end`, ...) that nm reports with
30+
/// nonsense sizes computed from the gap to the next symbol — these
31+
/// inflate the bloat report by gigabytes if not filtered.
32+
pub fn read_pt_load_regions(elf_path: &Path) -> Result<Vec<LoadedRegion>> {
33+
use object::read::elf::{ElfFile32, ElfFile64, FileHeader, ProgramHeader};
34+
use object::{Endianness, FileKind};
35+
36+
let bytes = std::fs::read(elf_path).map_err(|e| {
37+
FbuildError::BuildFailed(format!(
38+
"could not read ELF at {} for PT_LOAD probe: {e}",
39+
elf_path.display()
40+
))
41+
})?;
42+
let kind = FileKind::parse(&bytes[..]).map_err(|e| {
43+
FbuildError::BuildFailed(format!(
44+
"could not identify file kind for {}: {e}",
45+
elf_path.display()
46+
))
47+
})?;
48+
49+
let mut regions = Vec::new();
50+
match kind {
51+
FileKind::Elf32 => {
52+
let elf = ElfFile32::<Endianness>::parse(&bytes[..])
53+
.map_err(|e| FbuildError::BuildFailed(format!("ELF32 parse failed: {e}")))?;
54+
let endian = elf
55+
.elf_header()
56+
.endian()
57+
.map_err(|e| FbuildError::BuildFailed(format!("ELF32 endian probe failed: {e}")))?;
58+
for ph in elf.elf_program_headers() {
59+
if ph.p_type(endian) != object::elf::PT_LOAD {
60+
continue;
61+
}
62+
let start = u64::from(ph.p_vaddr(endian));
63+
let size = u64::from(ph.p_memsz(endian));
64+
if size == 0 {
65+
continue;
66+
}
67+
regions.push(LoadedRegion {
68+
start,
69+
end: start.saturating_add(size),
70+
});
71+
}
72+
}
73+
FileKind::Elf64 => {
74+
let elf = ElfFile64::<Endianness>::parse(&bytes[..])
75+
.map_err(|e| FbuildError::BuildFailed(format!("ELF64 parse failed: {e}")))?;
76+
let endian = elf
77+
.elf_header()
78+
.endian()
79+
.map_err(|e| FbuildError::BuildFailed(format!("ELF64 endian probe failed: {e}")))?;
80+
for ph in elf.elf_program_headers() {
81+
if ph.p_type(endian) != object::elf::PT_LOAD {
82+
continue;
83+
}
84+
let start = ph.p_vaddr(endian);
85+
let size = ph.p_memsz(endian);
86+
if size == 0 {
87+
continue;
88+
}
89+
regions.push(LoadedRegion {
90+
start,
91+
end: start.saturating_add(size),
92+
});
93+
}
94+
}
95+
other => {
96+
return Err(FbuildError::BuildFailed(format!(
97+
"expected ELF, got {other:?} at {}",
98+
elf_path.display()
99+
)));
100+
}
101+
}
102+
Ok(regions)
103+
}
104+
22105
/// Auto-detect the cross-toolchain prefix from the directory containing
23106
/// an `nm` binary. e.g. `xtensa-esp32s3-elf-nm` → prefix
24107
/// `xtensa-esp32s3-elf-`, so `xtensa-esp32s3-elf-c++filt` can be derived.
@@ -181,14 +264,38 @@ pub fn analyze_elf(cfg: AnalyzeConfig<'_>) -> Result<FineGrainedSymbolMap> {
181264
synth_mangled.clone()
182265
};
183266

184-
Ok(build_fine_grained_map_with_synth(
267+
let mut map = build_fine_grained_map_with_synth(
185268
elf_s,
186269
cfg.map_path.map(|p| p.to_string_lossy().to_string()),
187270
nm_rows,
188271
demangled,
189272
ranges,
190273
&synth_demangled,
191-
))
274+
);
275+
276+
// Strip symbols that nm enumerated but that don't actually consume
277+
// bytes in the final binary — most importantly linker-script
278+
// boundary markers (`__StackTop`, `__flash_arduino_end`, ...)
279+
// whose nm-reported "size" is the address gap to the next symbol
280+
// and can be multiple gigabytes. The PT_LOAD probe is best-effort;
281+
// when it fails (corrupt ELF, non-ELF input) we leave the map
282+
// unfiltered rather than poisoning the report with an error.
283+
match read_pt_load_regions(cfg.elf_path) {
284+
Ok(regions) if !regions.is_empty() => map.retain_loaded_symbols(&regions),
285+
Ok(_) => {
286+
tracing::warn!(
287+
"no PT_LOAD segments found in {}; emitting unfiltered symbol report",
288+
cfg.elf_path.display()
289+
);
290+
}
291+
Err(e) => {
292+
tracing::warn!(
293+
"PT_LOAD probe failed for {} ({e}); emitting unfiltered symbol report",
294+
cfg.elf_path.display()
295+
);
296+
}
297+
}
298+
Ok(map)
192299
}
193300

194301
/// Format a fine-grained per-symbol map as a human-readable text report

crates/fbuild-build/tests/avr_build.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,11 @@ fn build_uno_minimal() {
7575
return;
7676
}
7777

78-
// Use a temp build dir so we don't pollute the Python project
78+
// Use a temp build dir so we don't pollute the Python project.
79+
// `params.build_dir` is the resolved env-rooted dir per
80+
// `BuildLayout::resolve()`.
7981
let tmp = tempfile::TempDir::new().unwrap();
80-
let build_dir = tmp.path().join(".fbuild/build");
82+
let build_dir = tmp.path().join(".fbuild/build/uno/release");
8183

8284
let params = BuildParams {
8385
project_dir: project_dir.clone(),
@@ -172,7 +174,7 @@ fn compare_with_python_output() {
172174

173175
// Build with Rust
174176
let tmp = tempfile::TempDir::new().unwrap();
175-
let build_dir = tmp.path().join(".fbuild/build");
177+
let build_dir = tmp.path().join(".fbuild/build/uno/release");
176178

177179
let params = BuildParams {
178180
project_dir: project_dir.clone(),
@@ -258,7 +260,7 @@ void loop() {
258260
)
259261
.unwrap();
260262

261-
let build_dir = project_dir.join(".fbuild/build");
263+
let build_dir = project_dir.join(".fbuild/build/uno/release");
262264
let params = BuildParams {
263265
project_dir: project_dir.to_path_buf(),
264266
env_name: "uno".to_string(),
@@ -435,7 +437,7 @@ fn cache_survives_tar_extract_uno() {
435437
let cold_result = orchestrator
436438
.build(&uno_build_params(
437439
&proj_a,
438-
proj_a.join(".fbuild/build"),
440+
proj_a.join(".fbuild/build/uno/release"),
439441
true,
440442
))
441443
.expect("cold AVR build should succeed");
@@ -467,7 +469,7 @@ fn cache_survives_tar_extract_uno() {
467469
let same_project_warm = orchestrator
468470
.build(&uno_build_params(
469471
&proj_a,
470-
proj_a.join(".fbuild/build"),
472+
proj_a.join(".fbuild/build/uno/release"),
471473
false,
472474
))
473475
.expect("same-project warm build should succeed");
@@ -512,7 +514,7 @@ fn cache_survives_tar_extract_uno() {
512514
let warm_result = orchestrator
513515
.build(&uno_build_params(
514516
&proj_b,
515-
proj_b.join(".fbuild/build"),
517+
proj_b.join(".fbuild/build/uno/release"),
516518
false,
517519
))
518520
.expect("warm AVR build (post tar-extract) should succeed");

crates/fbuild-build/tests/eh_frame_strip_esp32.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use fbuild_build::{BuildOrchestrator, BuildParams};
1919
use fbuild_core::BuildProfile;
2020

2121
fn make_params(project_dir: &Path) -> BuildParams {
22-
let build_dir = project_dir.join(".fbuild/build");
22+
let build_dir = project_dir.join(".fbuild/build/esp32dev/release");
2323
BuildParams {
2424
project_dir: project_dir.to_path_buf(),
2525
env_name: "esp32dev".to_string(),

crates/fbuild-build/tests/esp32_build.rs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ void loop() {
6060
)
6161
.unwrap();
6262

63-
let build_dir = project_dir.join(".fbuild/build");
63+
let build_dir = project_dir.join(".fbuild/build/esp32dev/release");
6464
let params = BuildParams {
6565
project_dir: project_dir.to_path_buf(),
6666
env_name: "esp32dev".to_string(),
@@ -149,7 +149,7 @@ void loop() {
149149
)
150150
.unwrap();
151151

152-
let build_dir = project_dir.join(".fbuild/build");
152+
let build_dir = project_dir.join(".fbuild/build/esp32c6/release");
153153
let params = BuildParams {
154154
project_dir: project_dir.to_path_buf(),
155155
env_name: "esp32c6".to_string(),
@@ -231,7 +231,7 @@ void loop() {
231231
)
232232
.unwrap();
233233

234-
let build_dir = project_dir.join(".fbuild/build");
234+
let build_dir = project_dir.join(".fbuild/build/esp32c3/release");
235235
let params = BuildParams {
236236
project_dir: project_dir.to_path_buf(),
237237
env_name: "esp32c3".to_string(),
@@ -314,7 +314,7 @@ void loop() {
314314
)
315315
.unwrap();
316316

317-
let build_dir = project_dir.join(".fbuild/build");
317+
let build_dir = project_dir.join(".fbuild/build/esp32s3/release");
318318
let params = BuildParams {
319319
project_dir: project_dir.to_path_buf(),
320320
env_name: "esp32s3".to_string(),
@@ -387,7 +387,7 @@ fn build_esp32s3_fixture() {
387387
return;
388388
}
389389

390-
let build_dir = project_dir.join(".fbuild/build");
390+
let build_dir = project_dir.join(".fbuild/build/esp32s3/release");
391391
let params = BuildParams {
392392
project_dir: project_dir.clone(),
393393
env_name: "esp32s3".to_string(),
@@ -451,7 +451,7 @@ fn build_nightdriverstrip_demo() {
451451
}
452452

453453
let tmp = tempfile::TempDir::new().unwrap();
454-
let build_dir = tmp.path().join(".fbuild/build");
454+
let build_dir = tmp.path().join(".fbuild/build/demo/release");
455455

456456
let params = BuildParams {
457457
project_dir: project_dir.clone(),
@@ -549,7 +549,12 @@ fn incremental_build_at(project_dir: &std::path::Path, env_name: &str) {
549549
env_name: env_name.to_string(),
550550
clean: false,
551551
profile: BuildProfile::Release,
552-
build_dir: project_dir.join(".fbuild/build"),
552+
build_dir: fbuild_paths::BuildLayout::new(
553+
project_dir.to_path_buf(),
554+
env_name.to_string(),
555+
BuildProfile::Release,
556+
)
557+
.resolve(),
553558
verbose: true,
554559
jobs: None,
555560
generate_compiledb: false,
@@ -640,7 +645,12 @@ fn incremental_nightdriverstrip_one_file_changed() {
640645
env_name: env_name.to_string(),
641646
clean: false,
642647
profile: BuildProfile::Release,
643-
build_dir: project_dir.join(".fbuild/build"),
648+
build_dir: fbuild_paths::BuildLayout::new(
649+
project_dir.clone(),
650+
env_name.to_string(),
651+
BuildProfile::Release,
652+
)
653+
.resolve(),
644654
verbose: true,
645655
jobs: None,
646656
generate_compiledb: false,

crates/fbuild-build/tests/stm32_acceptance.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ fn stm32f103c8_blink_with_spi_auto_discovers_library_205_ac4() {
6060
)
6161
.unwrap();
6262

63-
let build_dir = project_dir.join(".fbuild/build");
63+
let build_dir = project_dir.join(".fbuild/build/stm32f103c8/release");
6464
let params = BuildParams {
6565
project_dir: project_dir.to_path_buf(),
6666
env_name: "stm32f103c8".to_string(),

crates/fbuild-build/tests/teensy30_acceptance.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ fn teensy30_analog_output_meets_205_ac2() {
7575
)
7676
.unwrap();
7777

78-
let build_dir = project_dir.join(".fbuild/build");
78+
let build_dir = project_dir.join(".fbuild/build/teensy30/release");
7979
let params = BuildParams {
8080
project_dir: project_dir.to_path_buf(),
8181
// WHY env_name = "teensy30": must match the [env:teensy30] key

0 commit comments

Comments
 (0)