Skip to content
Draft
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
6 changes: 4 additions & 2 deletions .github/workflows/dylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ jobs:
run: |
export PATH="${CARGO_HOME}/bin:${PATH}"
set +e
for attempt in 1 2 3; do
lint_count="$(find dylints -mindepth 2 -maxdepth 2 -name Cargo.toml | wc -l | tr -d ' ')"
max_attempts=$((lint_count + 2))
for attempt in $(seq 1 "$max_attempts"); do
cargo dylint --all -- --workspace --all-targets
rc=$?
if [ $rc -eq 0 ]; then
Expand Down Expand Up @@ -102,5 +104,5 @@ jobs:
fi
echo "Created @<toolchain> aliases; retrying cargo dylint (attempt $((attempt+1)))..."
done
echo "::error::cargo dylint did not succeed after 3 attempts"
echo "::error::cargo dylint did not succeed after ${max_attempts} attempts"
exit 1
42 changes: 34 additions & 8 deletions .github/workflows/loc-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,54 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Check .rs file line counts
shell: bash
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before || '' }}
run: |
set -euo pipefail
THRESHOLD=1000
ZERO_SHA=0000000000000000000000000000000000000000
base_ref="${BASE_SHA:-}"
if [[ -n "$base_ref" && "$base_ref" != "$ZERO_SHA" ]]; then
git fetch --no-tags --depth=1 origin "$base_ref" || true
else
base_ref=""
fi

base_line_count() {
local file="$1"
if [[ -z "$base_ref" ]] || ! git cat-file -e "${base_ref}:${file}" 2>/dev/null; then
echo ""
return
fi
git show "${base_ref}:${file}" | wc -l | tr -d ' '
}

violations=0
while IFS= read -r -d '' file; do
lines=$(wc -l < "$file")
rel="${file#./}"
lines=$(wc -l < "$file" | tr -d ' ')
if [ "$lines" -gt "$THRESHOLD" ]; then
base_lines="$(base_line_count "$rel")"
if [[ -n "$base_lines" && "$base_lines" -gt "$THRESHOLD" ]]; then
echo "BASELINE ${lines} ${file} (base already has ${base_lines} LOC)"
continue
fi
echo "::error file=${file}::${file} has ${lines} LOC (limit: ${THRESHOLD})"
echo "FAIL ${lines} ${file}"
violations=$((violations + 1))
fi
done < <(find . -type f -name '*.rs' \
-not -path './target/*' \
-not -path './.git/*' \
-print0)
done < <(find . \
\( -path './target' -o -path './.git' \) -prune -o \
-type f -name '*.rs' -print0)
if [ "$violations" -gt 0 ]; then
echo ""
echo "Found ${violations} .rs file(s) over ${THRESHOLD} LOC."
echo "Split them into smaller modules before merging."
echo "Found ${violations} new .rs file(s) over ${THRESHOLD} LOC."
echo "Split new large files into smaller modules before merging."
exit 1
fi
echo "All .rs files are within the ${THRESHOLD} LOC limit."
echo "No newly over-limit .rs files found."
5 changes: 5 additions & 0 deletions .github/workflows/template_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-D warnings"
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
# setup-soldr's default 5 GiB target free-space guard is too conservative
# for hosted per-board Linux builds after the runner image and caches load.
# A 2 GiB hard stop still catches genuinely exhausted runners without
# failing healthy jobs before the board build starts.
SOLDR_TARGET_BLOCK_FREE_GB: "2"

jobs:
build:
Expand Down
16 changes: 15 additions & 1 deletion .github/workflows/update-data.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
# single failure is non-fatal — the merger downstream sees only the
# sources that actually arrived intact;
# 4. runs the USB-VID merger → sorted `usb-vid.json` + conflict log +
# per-dataset manifest fragment;
# per-dataset manifest fragment + compact runtime `usb-vids.proto.zstd`;
# 5. runs the PlatformIO board merger → `pio-boards.json` (deep-union
# with the previously committed copy so transient field drops in
# `pio boards` don't lose data) + per-dataset manifest fragment;
Expand Down Expand Up @@ -538,6 +538,20 @@ jobs:
--out "${ONLINE_WORKTREE}/data/usb-vendors.tar.zst"
ls -la "${ONLINE_WORKTREE}/data/usb-vendors.tar.zst"

- name: Package usb-vids.proto.zstd (runtime USB VID:PID overlay)
id: package-usb-proto
if: steps.merge-usb.outcome == 'success'
# Full product-level runtime cache for fbuild CLI/device scans. This
# is derived after source priority has already been resolved into
# usb-vid.json; weak third-party rows only fill gaps left by
# first-party/vendor/generic sources and never override their winners.
run: |
uv run --no-project --script \
"${{ github.workspace }}/online-data-tools/build_usb_vid_proto.py" \
--upstream "${ONLINE_WORKTREE}/data/usb-vid.json" \
--out "${ONLINE_WORKTREE}/data/usb-vids.proto.zstd"
ls -la "${ONLINE_WORKTREE}/data/usb-vids.proto.zstd"

- name: Assemble manifest.json
id: build-manifest
# We rebuild the manifest whenever at least one dataset succeeded,
Expand Down
11 changes: 11 additions & 0 deletions ci/validate_boards.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@
}
)

# Intentional local corrections where fbuild's checked-in board asset is more
# specific than the current PlatformIO registry row. Keep this narrow: it is
# only for board-local facts that are also asserted by fbuild tests.
FBUILD_BUILD_FIELD_OVERRIDES = {
# FastLED/fbuild#905 split Unexpected Maker TinyS3 and FeatherS3 USB PIDs.
# PlatformIO 6.13.0 still reports FeatherS3 as 303A:80D0, which collides
# with TinyS3. The local board asset and board::tests_usb_vid pin 80D6.
"um_feathers3": {"pid": "0x80D6"},
}

MEGATINYCORE_EXTRA_FLAGS = (
"-DCLOCK_SOURCE=0",
'-DMEGATINYCORE="2.6.11"',
Expand Down Expand Up @@ -253,6 +263,7 @@ def validate_board(board_path: Path, pio_dir: Path) -> list[str] | None:
pio_build = pio_board.get("build", {})
if isinstance(pio_build, dict):
expected_build = extract_build(pio_build)
expected_build.update(FBUILD_BUILD_FIELD_OVERRIDES.get(board_id, {}))
actual_build = board.get("build", {})
# Strip intentional fbuild-only extensions from the actual side so
# they aren't reported as drift (see FBUILD_EXTENSION_BUILD_FIELDS).
Expand Down
6 changes: 4 additions & 2 deletions crates/fbuild-build/src/apollo3/orchestrator.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Apollo3 build orchestrator — wires together config, packages, compiler, linker.
//! Apollo3 build orchestrator — wires together config, packages, compiler, linker.
//!
//! Build phases:
//! 1. Parse platformio.ini
Expand Down Expand Up @@ -67,7 +67,9 @@ impl BuildOrchestrator for Apollo3Orchestrator {
crate::package_override::resolve_override(env, "framework-arduinoambiqapollo3")
});
let framework = match __ovr {
Some(o) => fbuild_packages::library::Apollo3Cores::with_override(&params.project_dir, o),
Some(o) => {
fbuild_packages::library::Apollo3Cores::with_override(&params.project_dir, o)
}
None => fbuild_packages::library::Apollo3Cores::new(&params.project_dir),
};
let framework_dir = fbuild_packages::Package::ensure_installed(&framework).await?;
Expand Down
23 changes: 11 additions & 12 deletions crates/fbuild-build/src/build_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,18 +293,17 @@ pub fn emit_build_info(project_dir: &Path, env_name: &str, info: &BuildInfo) ->
// pipeline code, but always under the daemon's tokio runtime.
// Bridge to the async `write_atomic` via `block_in_place`. Same
// pattern as `fbuild_packages::toolchain::esp32_metadata`.
let write_res =
if let Ok(handle) = tokio::runtime::Handle::try_current() {
tokio::task::block_in_place(|| {
handle.block_on(fbuild_core::fs::write_atomic(path, json.as_bytes()))
})
} else {
// No runtime — happens in unit tests of this module.
// Fall back to plain `std::fs::write`; the integration
// path always has a runtime so the atomic guarantee is
// preserved where it matters.
std::fs::write(path, &json)
};
let write_res = if let Ok(handle) = tokio::runtime::Handle::try_current() {
tokio::task::block_in_place(|| {
handle.block_on(fbuild_core::fs::write_atomic(path, json.as_bytes()))
})
} else {
// No runtime — happens in unit tests of this module.
// Fall back to plain `std::fs::write`; the integration
// path always has a runtime so the atomic guarantee is
// preserved where it matters.
std::fs::write(path, &json)
};
if let Err(e) = write_res {
tracing::warn!("failed to write {}: {}", path.display(), e);
}
Expand Down
11 changes: 4 additions & 7 deletions crates/fbuild-build/src/ch32v/ch32v_compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};

use fbuild_core::path::NormalizedPath;
use fbuild_core::{BuildProfile, Result};

use super::mcu_config::Ch32vMcuConfig;
Expand Down Expand Up @@ -101,13 +102,9 @@ impl Ch32vCompiler {
let Some(root) = self.framework_root.as_ref() else {
return false;
};
match (
std::fs::canonicalize(source).ok(),
std::fs::canonicalize(root).ok(),
) {
(Some(s), Some(r)) => s.starts_with(&r),
_ => source.starts_with(root),
}
let source = NormalizedPath::new(source).into_path_buf();
let root = NormalizedPath::new(root).into_path_buf();
source.starts_with(&root)
}
}

Expand Down
60 changes: 50 additions & 10 deletions crates/fbuild-build/src/ch32v/orchestrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,20 @@ fn resolve_variant_dir(
requested_variant: &str,
system_series: &str,
) -> PathBuf {
// OpenWCH d767162 adds CH32VM00X/CH32V006K8, but that variant currently
// references PB_* pins not defined by the CH32V006 build. Before that
// upstream directory existed, fbuild built this board with the CH32V003F4
// family fallback; keep that known-good path for the pinned core.
if requested_variant == "CH32VM00X/CH32V006K8" {
let fallback = framework_dir
.join("variants")
.join("CH32V00x")
.join("CH32V003F4");
if fallback.is_dir() {
return fallback;
}
}

let requested = framework_dir.join("variants").join(requested_variant);
if requested.is_dir() {
return requested;
Expand Down Expand Up @@ -333,13 +347,14 @@ fn resolve_variant_dir(
}

/// Map a series name to the system directory name in the OpenWCH core.
/// e.g. "ch32v003" -> "CH32V00x", "ch32v103" -> "CH32V10x", "ch32x035" -> "CH32X035"
/// e.g. "ch32v003" -> "CH32V00x", "ch32l103" -> "CH32L10x", "ch32x035" -> "CH32X035"
fn series_to_system_dir(series: &str) -> String {
let upper = series.to_uppercase();
if upper.len() >= 7 {
// CH32V series use the "replace last digit with x" pattern (CH32V00x, CH32V10x, etc.)
// CH32X/CH32L series use the exact uppercase name (CH32X035, CH32L103, etc.)
if upper.starts_with("CH32V") {
// CH32V/CH32L series use the "replace last digit with x" family
// directory pattern (CH32V00x, CH32V10x, CH32L10x, etc.).
// CH32X035 uses its exact uppercase directory name.
if upper.starts_with("CH32V") || upper.starts_with("CH32L") {
format!("{}x", &upper[..upper.len() - 1])
} else {
upper
Expand All @@ -358,6 +373,10 @@ pub fn is_ch32v_project(project_dir: &Path, env_name: &str) -> bool {
mod tests {
use super::*;

fn tempdir() -> tempfile::TempDir {
tempfile::TempDir::new_in(fbuild_paths::temp_subdir("fbuild-ch32v-tests")).unwrap()
}

#[test]
fn test_ch32v_orchestrator_platform() {
let orch = Ch32vOrchestrator;
Expand All @@ -366,7 +385,7 @@ mod tests {

#[test]
fn test_is_ch32v_project() {
let tmp = tempfile::TempDir::new().unwrap();
let tmp = tempdir();
std::fs::write(
tmp.path().join("platformio.ini"),
"[env:ch32v003]\nplatform = ch32v\nboard = genericCH32V003F4P6\nframework = arduino\n",
Expand All @@ -378,7 +397,7 @@ mod tests {

#[test]
fn test_is_not_ch32v_project() {
let tmp = tempfile::TempDir::new().unwrap();
let tmp = tempdir();
std::fs::write(
tmp.path().join("platformio.ini"),
"[env:uno]\nplatform = atmelavr\nboard = uno\nframework = arduino\n",
Expand All @@ -395,14 +414,15 @@ mod tests {
assert_eq!(series_to_system_dir("ch32v203"), "CH32V20x");
assert_eq!(series_to_system_dir("ch32v303"), "CH32V30x");
assert_eq!(series_to_system_dir("ch32v307"), "CH32V30x");
// CH32X/CH32L: exact uppercase name
// CH32L follows the same family-directory pattern as CH32V.
assert_eq!(series_to_system_dir("ch32l103"), "CH32L10x");
// CH32X: exact uppercase name
assert_eq!(series_to_system_dir("ch32x035"), "CH32X035");
assert_eq!(series_to_system_dir("ch32l103"), "CH32L103");
}

#[test]
fn test_resolve_variant_dir_falls_back_to_family_variant() {
let tmp = tempfile::TempDir::new().unwrap();
let tmp = tempdir();
let fallback = tmp
.path()
.join("variants")
Expand All @@ -414,9 +434,29 @@ mod tests {
assert_eq!(resolved, fallback);
}

#[test]
fn test_resolve_variant_dir_skips_broken_ch32v006_upstream_variant() {
let tmp = tempdir();
let requested = tmp
.path()
.join("variants")
.join("CH32VM00X")
.join("CH32V006K8");
let fallback = tmp
.path()
.join("variants")
.join("CH32V00x")
.join("CH32V003F4");
std::fs::create_dir_all(&requested).unwrap();
std::fs::create_dir_all(&fallback).unwrap();

let resolved = resolve_variant_dir(tmp.path(), "CH32VM00X/CH32V006K8", "CH32V00x");
assert_eq!(resolved, fallback);
}

#[test]
fn test_resolve_variant_h_ignores_missing_preferred_header() {
let tmp = tempfile::TempDir::new().unwrap();
let tmp = tempdir();
std::fs::write(tmp.path().join("variant_CH32V003F4.h"), "").unwrap();

let resolved = resolve_variant_h(tmp.path(), Some("variant_CH32V006K8.h"));
Expand Down
2 changes: 1 addition & 1 deletion crates/fbuild-build/src/compile_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
//! `fbuild-build` and don't have a handle to `DaemonContext`, so the
//! installed backend lives in a process-wide `OnceLock` set by the
//! daemon's `#[tokio::main]` before any compile fires. Reads
//! ([`get`]) are lock-free.
//! ([`get_global`]) are lock-free.

use std::sync::{Arc, OnceLock};

Expand Down
4 changes: 2 additions & 2 deletions crates/fbuild-build/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -751,8 +751,8 @@ pub async fn compile_source(
.clone()
.or_else(|| output.parent().map(Path::to_path_buf))
.unwrap_or_else(|| PathBuf::from("."));
let compile_env = fbuild_core::subprocess::compile_env_for_build(&build_scratch_root)
.unwrap_or_default();
let compile_env =
fbuild_core::subprocess::compile_env_for_build(&build_scratch_root).unwrap_or_default();
let compile_fut = svc.compile(compiler, sanitized, cwd, compile_env);
let outcome = tokio::time::timeout(std::time::Duration::from_secs(300), compile_fut)
.await
Expand Down
17 changes: 6 additions & 11 deletions crates/fbuild-build/src/compiler_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ use super::*;
/// the raw relative string — so gcc received absolute `cwd` + relative `-o`,
/// resolving to a doubled path whose parent directory (`core/`) was never
/// created.
#[test]
fn compile_path_contract_pairs_cwd_and_output_arg_for_282() {
#[tokio::test]
async fn compile_path_contract_pairs_cwd_and_output_arg_for_282() {
use crate::zccache::{compile_cwd_from_output, path_arg_for_compile_cwd};
let tmp = tempfile::tempdir().unwrap();
// Normalize the tempdir to the same form `compile_cwd_from_output` will
Expand All @@ -28,15 +28,10 @@ fn compile_path_contract_pairs_cwd_and_output_arg_for_282() {
// `compile_cwd_from_output` runs the result through `strip_unc_prefix`.
// Strip the prefix here too so both sides stay on the plain `C:\...`
// form.
let tmp_canon = {
let canon = std::fs::canonicalize(tmp.path()).unwrap();
let s = canon.to_string_lossy().into_owned();
if let Some(rest) = s.strip_prefix(r"\\?\") {
std::path::PathBuf::from(rest)
} else {
canon
}
};
let tmp_canon = fbuild_core::path::canonicalize_existing(tmp.path())
.await
.unwrap()
.into_path_buf();
// Workspace shape mirrors CI: <project>/.fbuild/build/<env>/quick/core
let workspace = tmp_canon.join("proj_for_282");
let core = workspace
Expand Down
Loading
Loading