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
4 changes: 4 additions & 0 deletions .github/workflows/dylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ jobs:
# Library UI tests invoke soldr cargo test separately, so preserve
# the prebuilt dylint-link directory for their linker config.
sed -n 's/^soldr: dylint prepare: tool bin dir //p' "$RUNNER_TEMP/dylint-prepare.log" >> "$GITHUB_PATH"
# setup-soldr prepares the nightly with a minimal profile. The Dylint
# library formatting check below needs rustfmt on that exact nightly.
- name: Install rustfmt for the Dylint toolchain
run: soldr rustup component add --toolchain nightly-2026-05-28 rustfmt
- name: Install cross-target nightly standard libraries
if: ${{ inputs.ref != '' || github.event_name == 'workflow_dispatch' }}
run: |
Expand Down
158 changes: 130 additions & 28 deletions crates/fbuild-build-engine/src/package_override.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ pub enum PlatformDefaultVersion<'a> {
},
}

struct RegistryResolutionAccess<'a> {
client: &'a fbuild_packages::platformio_registry::RegistryClient,
cache_root: &'a Path,
fetch: bool,
}

/// Resolve a pinned PlatformIO platform and its selected package requirements
/// before a family adapter constructs its framework/toolchain packages. The
/// registry lookup and manifest parsing are intentionally independent of the
Expand All @@ -61,6 +67,33 @@ pub async fn resolve_registry_overrides(
.await
}

/// Resolve only from cached registry metadata and an installed platform
/// manifest. `fbuild install --check/--dry-run` must never fetch a platform.
/// `None` means a required payload or platform archive is not cached yet.
pub async fn resolve_registry_overrides_offline(
project_dir: &Path,
env_config: &HashMap<String, String>,
platform_name: &str,
package_names: &[&str],
platform_default_requirements: &[(&str, PlatformDefaultVersion<'_>)],
) -> fbuild_core::Result<Option<HashMap<String, PackageOverride>>> {
let cache_root = fbuild_packages::Cache::new(project_dir).platforms_dir();
let client = fbuild_packages::platformio_registry::RegistryClient::default();
resolve_registry_overrides_with_client_fetch(
project_dir,
env_config,
platform_name,
package_names,
platform_default_requirements,
RegistryResolutionAccess {
client: &client,
cache_root: &cache_root,
fetch: false,
},
)
.await
}

async fn resolve_registry_overrides_with_client(
project_dir: &Path,
env_config: &HashMap<String, String>,
Expand All @@ -70,6 +103,30 @@ async fn resolve_registry_overrides_with_client(
client: &fbuild_packages::platformio_registry::RegistryClient,
cache_root: &Path,
) -> fbuild_core::Result<HashMap<String, PackageOverride>> {
resolve_registry_overrides_with_client_fetch(
project_dir,
env_config,
platform_name,
package_names,
platform_default_requirements,
RegistryResolutionAccess {
client,
cache_root,
fetch: true,
},
)
.await?
.ok_or_else(|| package_error("PlatformIO registry payload unavailable"))
}

async fn resolve_registry_overrides_with_client_fetch(
project_dir: &Path,
env_config: &HashMap<String, String>,
platform_name: &str,
package_names: &[&str],
platform_default_requirements: &[(&str, PlatformDefaultVersion<'_>)],
access: RegistryResolutionAccess<'_>,
) -> fbuild_core::Result<Option<HashMap<String, PackageOverride>>> {
let raw_platform = env_config
.get("platform")
.map(String::as_str)
Expand Down Expand Up @@ -115,7 +172,7 @@ async fn resolve_registry_overrides_with_client(
.is_some()
|| explicit.iter().any(|spec| spec.registry().is_some());
if !has_registry_pin {
return Ok(HashMap::new());
return Ok(Some(HashMap::new()));
}
let host = fbuild_core::platformio_package::host_system(fbuild_core::platform::host::current())
.ok_or_else(|| {
Expand All @@ -137,19 +194,23 @@ async fn resolve_registry_overrides_with_client(
// native adapter's existing default stack.
explicit_registry_requirements(&explicit)
} else {
let platform = client
let platform = access
.client
.resolve_cached(
&platform_registry,
PackageKind::Platform,
host,
cache_root,
true,
access.cache_root,
access.fetch,
)
.await
.map_err(package_error)?
.ok_or_else(|| {
fbuild_core::FbuildError::PackageError("PlatformIO platform unavailable".into())
})?;
.map_err(package_error)?;
let Some(platform) = platform else {
if !access.fetch {
return Ok(None);
}
return Err(package_error("PlatformIO platform unavailable"));
};
tracing::info!(
"resolved requested PlatformIO platform {}@{}: {} (sha256 {})",
platform.name,
Expand All @@ -166,17 +227,23 @@ async fn resolve_registry_overrides_with_client(
fbuild_packages::CacheSubdir::Platforms,
project_dir,
);
let installed = platform_base
.staged_install(|dir| {
find_platform_manifest(dir).ok_or_else(|| {
fbuild_core::FbuildError::PackageError(format!(
"{} has no platform.json",
dir.display()
))
})?;
Ok(())
})
.await?;
let installed = if access.fetch {
platform_base
.staged_install(|dir| {
find_platform_manifest(dir).ok_or_else(|| {
fbuild_core::FbuildError::PackageError(format!(
"{} has no platform.json",
dir.display()
))
})?;
Ok(())
})
.await?
} else if platform_base.is_cached() {
platform_base.install_path()
} else {
return Ok(None);
};
let manifest_path = find_platform_manifest(&installed).ok_or_else(|| {
fbuild_core::FbuildError::PackageError(format!(
"{} has no platform.json",
Expand Down Expand Up @@ -239,15 +306,25 @@ async fn resolve_registry_overrides_with_client(
let Some(registry) = requirement.spec.registry() else {
continue;
};
let payload = client
.resolve_cached(registry, requirement.kind, host, cache_root, true)
let payload = access
.client
.resolve_cached(
registry,
requirement.kind,
host,
access.cache_root,
access.fetch,
)
.await
.map_err(package_error)?
.ok_or_else(|| {
fbuild_core::FbuildError::PackageError(format!(
"PlatformIO package `{name}` unavailable"
))
})?;
.map_err(package_error)?;
let Some(payload) = payload else {
if !access.fetch {
return Ok(None);
}
return Err(package_error(format!(
"PlatformIO package `{name}` unavailable"
)));
};
tracing::info!(
"resolved PlatformIO package {}@{}: {} (sha256 {})",
payload.name,
Expand All @@ -264,7 +341,7 @@ async fn resolve_registry_overrides_with_client(
},
);
}
Ok(overrides)
Ok(Some(overrides))
}

fn requirements_with_platform_defaults(
Expand Down Expand Up @@ -804,4 +881,29 @@ mod tests {
.contains("framework-arduino-unknown")
);
}

#[tokio::test]
async fn offline_registry_resolution_does_not_contact_uncached_platform() {
let temp = tempfile::tempdir().unwrap();
let client = fbuild_packages::platformio_registry::RegistryClient::new(
"http://127.0.0.1:1/v3",
reqwest::Client::new(),
);
let env = env(&[("platform", "platformio/espressif8266@4.0.1")]);
let result = resolve_registry_overrides_with_client_fetch(
temp.path(),
&env,
"espressif8266",
&["framework-arduinoespressif8266", "toolchain-xtensa"],
&[],
RegistryResolutionAccess {
client: &client,
cache_root: &temp.path().join("uncached-platform-metadata"),
fetch: false,
},
)
.await
.unwrap();
assert!(result.is_none());
}
}
6 changes: 6 additions & 0 deletions crates/fbuild-build-engine/src/pipeline/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ impl BuildContext {
Some(fbuild_core::Platform::Espressif32) => {
Some(fbuild_core::Platform::Espressif32)
}
Some(fbuild_core::Platform::Espressif8266) => {
Some(fbuild_core::Platform::Espressif8266)
}
Some(fbuild_core::Platform::Teensy) => Some(fbuild_core::Platform::Teensy),
Some(fbuild_core::Platform::Ststm32) => Some(fbuild_core::Platform::Ststm32),
Some(fbuild_core::Platform::NordicNrf52) => {
Expand All @@ -194,6 +197,9 @@ impl BuildContext {
};
let resolved_packages: &[&str] = match resolved_platform {
Some(fbuild_core::Platform::Espressif32) => &["framework-arduinoespressif32"],
Some(fbuild_core::Platform::Espressif8266) => {
&["framework-arduinoespressif8266", "toolchain-xtensa"]
}
Some(fbuild_core::Platform::Teensy) => {
&["framework-arduinoteensy", "toolchain-gccarmnoneeabi-teensy"]
}
Expand Down
31 changes: 27 additions & 4 deletions crates/fbuild-build-esp/src/esp8266/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! ESP8266 platform build support (NodeMCU, Wemos D1, etc.)
//! ESP8266 platform build support (NodeMCU, Wemos D1, etc.)

pub mod esp8266_compiler;
pub mod esp8266_linker;
Expand All @@ -23,9 +23,32 @@ impl crate::PlatformSupport for Esp8266PlatformSupport {
inputs: &crate::provision::ProvisionInputs<'_>,
mode: crate::provision::ProvisionMode,
) -> fbuild_core::Result<Vec<crate::provision::ProvisionedPackage>> {
use crate::provision::{PackageKind, provision_package};
let (toolchain, framework) =
orchestrator::esp8266_packages(inputs.project_dir, Some(inputs.env_config));
use crate::provision::{
PackageKind, ProvisionStatus, ProvisionedPackage, provision_package,
};
let selected = if mode.fetches() {
Some(orchestrator::esp8266_packages(inputs.project_dir, Some(inputs.env_config)).await?)
} else {
orchestrator::esp8266_packages_offline(inputs.project_dir, Some(inputs.env_config))
.await?
};
let Some((toolchain, framework)) = selected else {
// Metadata or the selected platform manifest is not cached.
// These modes cannot fetch it, so report uncertainty as missing
// instead of silently reporting the adapter's default packages.
return Ok(vec![
ProvisionedPackage::new(
PackageKind::Toolchain,
"toolchain-xtensa",
ProvisionStatus::WouldFetch,
),
ProvisionedPackage::new(
PackageKind::Framework,
"framework-arduinoespressif8266",
ProvisionStatus::WouldFetch,
),
]);
};
Ok(vec![
provision_package(PackageKind::Toolchain, &toolchain, mode).await,
provision_package(PackageKind::Framework, &framework, mode).await,
Expand Down
Loading
Loading