use crate::backend::platform_target::PlatformTarget;
use crate::cli::args::BackendArg;
use crate::cmd::CmdLineRunner;
use crate::config::Settings;
use crate::http::{HTTP, HTTP_FETCH};
use crate::install_context::InstallContext;
use crate::lockfile::PlatformInfo;
use crate::platform::linux_os_release;
use crate::toolset::{ToolRequest, ToolVersion};
use crate::ui::progress_report::SingleReport;
use crate::{backend::Backend, backend::VersionInfo, config::Config};
use crate::{file, github, gpg, plugins};
use async_trait::async_trait;
use eyre::{Result, bail, eyre};
use std::{
collections::BTreeMap,
path::{Path, PathBuf},
sync::Arc,
};
use tempfile::tempdir_in;
const SWIFT_PLATFORM_OPTION: &str = "swift_platform";
#[derive(Debug)]
pub(super) struct SwiftPlugin {
ba: Arc<BackendArg>,
}
impl SwiftPlugin {
pub(super) fn new() -> Self {
Self {
ba: Arc::new(plugins::core::new_backend_arg("swift")),
}
}
fn swift_bin(&self, tv: &ToolVersion) -> PathBuf {
tv.install_path().join("bin").join(swift_bin_name())
}
fn test_swift(&self, ctx: &InstallContext, tv: &ToolVersion) -> Result<()> {
ctx.pr.set_message("swift --version".into());
CmdLineRunner::new(self.swift_bin(tv))
.with_pr(ctx.pr.as_ref())
.arg("--version")
.env_values(tv.install_env())
.execute()
}
async fn download(
&self,
tv: &ToolVersion,
url: &str,
pr: &dyn SingleReport,
) -> Result<PathBuf> {
let filename = url.split('/').next_back().unwrap();
let tarball_path = tv.download_path().join(filename);
if !tarball_path.exists() {
pr.set_message(format!("download {filename}"));
HTTP.download_file(url, &tarball_path, Some(pr)).await?;
}
Ok(tarball_path)
}
fn install(&self, ctx: &InstallContext, tv: &ToolVersion, tarball_path: &Path) -> Result<()> {
let filename = tarball_path.file_name().unwrap().to_string_lossy();
let version = &tv.version;
ctx.pr.set_message(format!("extract {filename}"));
if cfg!(macos) {
let tmp = {
tempdir_in(tv.install_path().parent().unwrap())?
.path()
.to_path_buf()
};
CmdLineRunner::new(pkgutil_path())
.arg("--expand-full")
.arg(tarball_path)
.arg(&tmp)
.with_pr(ctx.pr.as_ref())
.env_values(tv.install_env())
.execute()?;
file::remove_all(tv.install_path())?;
file::rename(
tmp.join(format!("swift-{version}-RELEASE-osx-package.pkg"))
.join("Payload"),
tv.install_path(),
)?;
} else if cfg!(windows) {
todo!("install from exe");
} else {
file::untar(
tarball_path,
&tv.install_path(),
file::ExtractionFormat::TarGz,
&file::ExtractOptions {
strip_components: 1,
pr: Some(ctx.pr.as_ref()),
..Default::default()
},
)?;
}
Ok(())
}
fn symlink_bins(&self, tv: &ToolVersion) -> Result<()> {
let usr_bin = tv.install_path().join("usr").join("bin");
let bin_dir = tv.install_path().join("bin");
file::create_dir_all(&bin_dir)?;
for bin in file::ls(&usr_bin)? {
if !file::is_executable(&bin) {
continue;
}
let file_name = bin.file_name().unwrap().to_string_lossy().to_string();
if file_name.contains("swift") || file_name.contains("sourcekit") {
file::make_symlink_or_copy(&bin, &bin_dir.join(file_name))?;
}
}
Ok(())
}
async fn verify_gpg(&self, ctx: &InstallContext, url: &str, tarball_path: &Path) -> Result<()> {
let sig_path = PathBuf::from(format!("{}.sig", tarball_path.to_string_lossy()));
HTTP.download_file(format!("{url}.sig"), &sig_path, Some(ctx.pr.as_ref()))
.await?;
let signature = file::read(&sig_path)?;
gpg::verify_swift(tarball_path, &signature)?;
Ok(())
}
fn verify(&self, ctx: &InstallContext, tv: &ToolVersion) -> Result<()> {
self.test_swift(ctx, tv)
.map_err(|err| explain_missing_libraries(tv, err))
}
}
#[cfg(target_os = "linux")]
fn explain_missing_libraries(tv: &ToolVersion, err: eyre::Report) -> eyre::Report {
let missing = missing_sonames(&tv.install_path(), &tv.install_env());
if missing.is_empty() {
return err;
}
err.wrap_err(format!(
"this swift build needs shared libraries missing from this host: {} (point LD_LIBRARY_PATH at them with install_env)",
missing.join(", ")
))
}
#[cfg(not(target_os = "linux"))]
fn explain_missing_libraries(_tv: &ToolVersion, err: eyre::Report) -> eyre::Report {
err
}
#[cfg(target_os = "linux")]
fn missing_sonames(
install_path: &Path,
install_env: &indexmap::IndexMap<String, crate::config::env_directive::EnvValue>,
) -> Vec<String> {
if file::which("ldd").is_none() {
debug!("swift: no ldd on PATH, cannot name the missing libraries");
return vec![];
}
let mut missing = std::collections::BTreeSet::new();
for (dir, want_library) in [("usr/bin", false), ("usr/lib", true)] {
for entry in file::ls(&install_path.join(dir)).unwrap_or_default() {
let is_candidate = if want_library {
entry
.file_name()
.is_some_and(|name| name.to_string_lossy().contains(".so"))
} else {
file::is_executable(&entry)
};
if !is_candidate || !entry.is_file() {
continue;
}
let mut ldd = crate::cmd::cmd("ldd", [entry.as_os_str()])
.unchecked()
.stderr_null();
for (key, value) in install_env.clone() {
ldd = match value.into_string() {
Some(value) => ldd.env(key, value),
None => ldd.env_remove(key),
};
}
match ldd.read() {
Ok(output) => missing.extend(parse_ldd_missing(&output)),
Err(err) => debug!("swift: ldd {}: {err:#}", file::display_path(&entry)),
}
}
}
missing.into_iter().collect()
}
#[cfg(target_os = "linux")]
fn parse_ldd_missing(output: &str) -> Vec<String> {
output
.lines()
.filter_map(|line| {
let line = line.trim();
line.ends_with("=> not found")
.then(|| line.split_whitespace().next())
.flatten()
.map(str::to_string)
})
.collect()
}
#[cfg(macos)]
fn pkgutil_path() -> PathBuf {
resolve_pkgutil_path(file::which("pkgutil"))
}
#[cfg(not(macos))]
fn pkgutil_path() -> PathBuf {
PathBuf::from("pkgutil")
}
#[cfg(macos)]
fn resolve_pkgutil_path(which_result: Option<PathBuf>) -> PathBuf {
if let Some(path) = which_result {
return path;
}
let fallback = PathBuf::from("/usr/sbin/pkgutil");
if file::is_executable(&fallback) {
fallback
} else {
PathBuf::from("pkgutil")
}
}
#[cfg(all(test, macos))]
mod tests {
use super::resolve_pkgutil_path;
use crate::file;
use std::path::PathBuf;
#[test]
fn resolve_pkgutil_path_prefers_discovered_path() {
let discovered = PathBuf::from("/tmp/custom/pkgutil");
assert_eq!(resolve_pkgutil_path(Some(discovered.clone())), discovered);
}
#[test]
fn resolve_pkgutil_path_falls_back_to_system_location() {
let resolved = resolve_pkgutil_path(None);
let fallback = PathBuf::from("/usr/sbin/pkgutil");
if file::is_executable(&fallback) {
assert_eq!(resolved, fallback);
} else {
assert_eq!(resolved, PathBuf::from("pkgutil"));
}
}
}
#[async_trait]
impl Backend for SwiftPlugin {
fn ba(&self) -> &Arc<BackendArg> {
&self.ba
}
fn supports_lockfile_url(&self) -> bool {
false
}
fn resolve_lockfile_options(
&self,
_request: &ToolRequest,
target: &PlatformTarget,
) -> Result<BTreeMap<String, String>> {
let mut opts = BTreeMap::new();
if target.os_name() == "linux" {
let label = match &Settings::get().swift.platform {
Some(pinned) => pinned.clone(),
None => host_distro(target).label(),
};
opts.insert(SWIFT_PLATFORM_OPTION.to_string(), label);
}
Ok(opts)
}
fn lockfile_options_are_host_specific(&self) -> bool {
true
}
async fn resolve_lock_info(
&self,
tv: &ToolVersion,
target: &PlatformTarget,
) -> Result<PlatformInfo> {
if target.libc() == Some("musl") {
bail!("swift does not publish musl builds");
}
let url = url(tv, target, &resolve_platform(tv, target).await?);
if let Err(err) = HTTP.head(&url).await {
bail!("swift does not publish {url}: {err}");
}
Ok(PlatformInfo {
url: Some(url),
..Default::default()
})
}
async fn security_info(&self) -> Vec<crate::backend::SecurityFeature> {
use crate::backend::SecurityFeature;
let mut features = vec![SecurityFeature::Checksum {
algorithm: Some("sha256".to_string()),
}];
if cfg!(target_os = "linux") && Settings::get().swift.gpg_verify != Some(false) {
features.push(SecurityFeature::Gpg);
}
features
}
async fn _list_remote_versions(&self, _config: &Arc<Config>) -> Result<Vec<VersionInfo>> {
let versions = github::list_releases("swiftlang/swift")
.await?
.into_iter()
.filter_map(|r| {
let released_at = r.released_at().to_string();
r.tag_name
.strip_prefix("swift-")
.and_then(|v| v.strip_suffix("-RELEASE"))
.map(|v| (v.to_string(), released_at))
})
.rev()
.map(|(version, created_at)| VersionInfo {
version,
created_at: Some(created_at),
..Default::default()
})
.collect();
Ok(versions)
}
async fn install_version_(
&self,
ctx: &InstallContext,
mut tv: ToolVersion,
) -> Result<ToolVersion> {
let target = PlatformTarget::from_current();
let url = url(&tv, &target, &resolve_platform(&tv, &target).await?);
let tarball_path = self.download(&tv, &url, ctx.pr.as_ref()).await?;
if cfg!(target_os = "linux") && Settings::get().swift.gpg_verify != Some(false) {
self.verify_gpg(ctx, &url, &tarball_path).await?;
}
self.verify_checksum(ctx, &mut tv, &tarball_path)?;
self.install(ctx, &tv, &tarball_path)?;
self.symlink_bins(&tv)?;
self.verify(ctx, &tv)?;
Ok(tv)
}
}
fn swift_bin_name() -> &'static str {
if cfg!(windows) { "swift.exe" } else { "swift" }
}
fn platform_directory(target: &PlatformTarget, platform: &str) -> String {
let directory = match target.os_name() {
"macos" => "xcode".to_string(),
"windows" => "windows10".to_string(),
_ => platform.replace(".", ""),
};
match architecture(target) {
Some(arch) => format!("{directory}-{arch}"),
None => directory,
}
}
async fn resolve_platform(tv: &ToolVersion, target: &PlatformTarget) -> Result<String> {
match target.os_name() {
"macos" => Ok("osx".to_string()),
"windows" => Ok("windows10".to_string()),
_ => {
if target.libc() == Some("musl") {
bail!("swift does not publish musl builds");
}
if let Some(pinned) = &Settings::get().swift.platform {
return Ok(pinned.clone());
}
let arch = api_arch(target);
let host = host_distro(target);
match fetch_linux_builds(&tv.version).await {
Ok(builds) => match select_build(&builds, &host, arch) {
Some((build, fit)) => {
if target.is_current() {
warn_about_fit(&tv.version, &host, build, fit);
}
Ok(build.token.clone())
}
None => bail!("swift {} publishes no Linux build for {arch}", tv.version),
},
Err(err) => bail!(
"swift {}: cannot tell which Linux build {} needs without swift.org's release index: {err:#}\nSet swift.platform to choose a build without it.",
tv.version,
host.id
),
}
}
}
}
fn host_distro(target: &PlatformTarget) -> HostDistro {
if !target.is_current() {
return HostDistro {
family: Family::Ubuntu,
version: Some(DistroVersion::new(DEFAULT_UBUNTU_VERSION)),
family_is_fallback: false,
id: format!("ubuntu {DEFAULT_UBUNTU_VERSION}"),
};
}
let os_release = linux_os_release();
os_release
.and_then(HostDistro::from_os_release)
.unwrap_or_else(|| {
let id = os_release.map(os_release_id).unwrap_or_default();
HostDistro::unrecognized(id)
})
}
const RELEASES_URL: &str = "https://www.swift.org/api/v1/install/releases.json";
#[derive(Debug, serde::Deserialize)]
struct ApiRelease {
name: String,
#[serde(default)]
platforms: Vec<ApiPlatform>,
}
#[derive(Debug, serde::Deserialize)]
struct ApiPlatform {
name: String,
platform: String,
#[serde(default)]
dir: Option<String>,
#[serde(default)]
archs: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Family {
Ubuntu,
Debian,
Fedora,
AmazonLinux,
Ubi,
}
impl Family {
const DISPLAY_PREFIXES: &'static [(&'static str, Family)] = &[
("Ubuntu ", Family::Ubuntu),
("Debian ", Family::Debian),
("Fedora ", Family::Fedora),
("Amazon Linux ", Family::AmazonLinux),
("Red Hat Universal Base Image ", Family::Ubi),
];
fn from_distro_id(id: &str) -> Option<Self> {
match id {
"ubuntu" => Some(Family::Ubuntu),
"debian" | "raspbian" => Some(Family::Debian),
"fedora" => Some(Family::Fedora),
"amzn" => Some(Family::AmazonLinux),
"rhel" | "ubi" | "centos" | "rocky" | "almalinux" | "ol" => Some(Family::Ubi),
_ => None,
}
}
fn token_prefix(self) -> &'static str {
match self {
Family::Ubuntu => "ubuntu",
Family::Debian => "debian",
Family::Fedora => "fedora",
Family::AmazonLinux => "amazonlinux",
Family::Ubi => "ubi",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct LinuxBuild {
token: String,
family: Family,
version: DistroVersion,
archs: Vec<String>,
}
impl LinuxBuild {
fn from_api(platform: &ApiPlatform) -> Option<Self> {
if platform.platform != "Linux" {
return None;
}
let (prefix, family) = Family::DISPLAY_PREFIXES
.iter()
.find(|(prefix, _)| platform.name.starts_with(prefix))?;
let version = DistroVersion::new(platform.name.strip_prefix(prefix)?);
let token = match &platform.dir {
Some(dir) => dir.clone(),
None => platform.name.to_lowercase().replace(' ', ""),
};
Some(Self {
token,
family: *family,
version,
archs: platform.archs.clone(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct DistroVersion {
raw: String,
parts: Vec<u64>,
}
impl DistroVersion {
fn new(raw: &str) -> Self {
Self {
raw: raw.to_string(),
parts: raw
.split('.')
.map(|part| part.parse().unwrap_or(0))
.collect(),
}
}
fn major_only(&self) -> Self {
Self::new(self.raw.split('.').next().unwrap_or(&self.raw))
}
}
impl Ord for DistroVersion {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.parts.cmp(&other.parts)
}
}
impl PartialOrd for DistroVersion {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct HostDistro {
family: Family,
version: Option<DistroVersion>,
family_is_fallback: bool,
id: String,
}
impl HostDistro {
fn unrecognized(id: String) -> Self {
Self {
family: FALLBACK_FAMILY,
version: None,
family_is_fallback: true,
id,
}
}
}
fn os_release_id(os_release: &crate::platform::LinuxOsRelease) -> String {
if os_release.version_id.is_empty() {
os_release.id.clone()
} else {
format!("{} {}", os_release.id, os_release.version_id)
}
}
impl HostDistro {
fn from_os_release(os_release: &crate::platform::LinuxOsRelease) -> Option<Self> {
let mut ids = os_release.ids();
let own_id = ids.next();
if let Some(family) = own_id.and_then(Family::from_distro_id) {
let version = (!os_release.version_id.is_empty()).then(|| {
let version = DistroVersion::new(&os_release.version_id);
if family == Family::Ubi {
version.major_only()
} else {
version
}
});
return Some(Self {
family,
version,
family_is_fallback: false,
id: os_release_id(os_release),
});
}
ids.find_map(Family::from_distro_id).map(|family| Self {
family,
version: None,
family_is_fallback: false,
id: os_release_id(os_release),
})
}
fn label(&self) -> String {
let prefix = self.family.token_prefix();
match &self.version {
Some(version) => format!("{prefix}{}", version.raw),
None => prefix.to_string(),
}
}
}
const FALLBACK_FAMILY: Family = Family::Ubi;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Fit {
Exact,
OlderThanHost,
UnknownVersion,
NewerThanHost,
OtherFamily,
}
fn select_build<'a>(
builds: &'a [LinuxBuild],
host: &HostDistro,
arch: &str,
) -> Option<(&'a LinuxBuild, Fit)> {
let candidates = |family: Family| {
let mut matching = builds
.iter()
.filter(|build| build.family == family && build.archs.iter().any(|a| a == arch))
.collect::<Vec<_>>();
matching.sort_by(|a, b| a.version.cmp(&b.version));
matching
};
let pick = |family: Family, version: Option<&DistroVersion>| -> Option<(&'a LinuxBuild, Fit)> {
let matching = candidates(family);
let Some(version) = version else {
return matching
.first()
.copied()
.map(|build| (build, Fit::UnknownVersion));
};
if let Some(exact) = matching.iter().find(|build| &build.version == version) {
return Some((exact, Fit::Exact));
}
if let Some(older) = matching.iter().rev().find(|build| &build.version < version) {
return Some((older, Fit::OlderThanHost));
}
matching
.first()
.copied()
.map(|build| (build, Fit::NewerThanHost))
};
let own_family = (!host.family_is_fallback)
.then(|| pick(host.family, host.version.as_ref()))
.flatten();
own_family
.or_else(|| pick(FALLBACK_FAMILY, None).map(|(build, _)| (build, Fit::OtherFamily)))
.or_else(|| {
let mut any = builds
.iter()
.filter(|build| build.archs.iter().any(|a| a == arch))
.collect::<Vec<_>>();
any.sort_by(|a, b| a.version.cmp(&b.version));
any.first().copied().map(|build| (build, Fit::OtherFamily))
})
}
fn warn_about_fit(version: &str, host: &HostDistro, build: &LinuxBuild, fit: Fit) {
match fit {
Fit::Exact | Fit::OlderThanHost => {}
Fit::UnknownVersion => warn!(
"swift {version}: {} does not say which {} release it is based on; using {}",
host.id,
build.family.token_prefix(),
build.token
),
Fit::NewerThanHost => warn!(
"swift {version} publishes no build for {} or anything older; using {} and it may not run here",
host.id, build.token
),
Fit::OtherFamily => warn!(
"swift {version} publishes no build for {}; using {}",
host.id, build.token
),
}
}
fn api_arch(target: &PlatformTarget) -> &str {
match target.arch_name() {
"x64" => "x86_64",
"arm64" => "aarch64",
other => other,
}
}
async fn fetch_linux_builds(version: &str) -> Result<Vec<LinuxBuild>> {
let releases: Vec<ApiRelease> = HTTP_FETCH.json_cached(RELEASES_URL).await?;
let release = releases
.iter()
.find(|release| release.name == version)
.ok_or_else(|| eyre!("swift.org publishes no release named {version}"))?;
Ok(release
.platforms
.iter()
.filter_map(LinuxBuild::from_api)
.collect())
}
fn extension(target: &PlatformTarget) -> &'static str {
match target.os_name() {
"macos" => "pkg",
"windows" => "exe",
_ => "tar.gz",
}
}
fn architecture(target: &PlatformTarget) -> Option<&str> {
let arch = target.arch_name();
match target.os_name() {
"linux" => match arch {
"x64" => None,
"arm64" => Some("aarch64"),
_ => Some(arch),
},
"windows" if arch == "arm64" => Some("arm64"),
_ => None,
}
}
const DEFAULT_UBUNTU_VERSION: &str = "24.04";
fn url(tv: &ToolVersion, target: &PlatformTarget, platform: &str) -> String {
format!(
"https://download.swift.org/swift-{version}-release/{platform_directory}/swift-{version}-RELEASE/swift-{version}-RELEASE-{platform}{architecture}.{extension}",
version = tv.version,
platform_directory = platform_directory(target, platform),
extension = extension(target),
architecture = match architecture(target) {
Some(arch) => format!("-{arch}"),
None => "".into(),
}
)
}
#[cfg(test)]
mod platform_selection_tests {
use super::*;
use crate::platform::LinuxOsRelease;
const RELEASES_FIXTURE: &str = r#"[
{
"name": "6.3.3",
"platforms": [
{"name": "Ubuntu 22.04", "platform": "Linux", "archs": ["x86_64", "aarch64"]},
{"name": "Ubuntu 24.04", "platform": "Linux", "archs": ["x86_64", "aarch64"]},
{"name": "Debian 12", "platform": "Linux", "archs": ["x86_64", "aarch64"]},
{"name": "Fedora 39", "platform": "Linux", "archs": ["x86_64", "aarch64"]},
{"name": "Fedora 41", "platform": "Linux", "archs": ["x86_64", "aarch64"]},
{"name": "Amazon Linux 2", "platform": "Linux", "archs": ["x86_64", "aarch64"]},
{"name": "Red Hat Universal Base Image 9", "platform": "Linux", "dir": "ubi9",
"archs": ["x86_64", "aarch64"]},
{"name": "Windows 10", "platform": "Windows", "archs": ["x86_64", "arm64"]},
{"name": "Static SDK", "platform": "static-sdk", "archs": ["x86_64", "arm64"]}
]
},
{
"name": "6.4.0",
"platforms": [
{"name": "Ubuntu 24.04", "platform": "Linux", "archs": ["x86_64", "aarch64"]},
{"name": "Fedora 41", "platform": "Linux", "archs": ["x86_64"]},
{"name": "Amazon Linux 2023", "platform": "Linux", "archs": ["x86_64", "aarch64"]},
{"name": "Red Hat Universal Base Image 9", "platform": "Linux", "dir": "ubi9",
"archs": ["x86_64", "aarch64"]},
{"name": "Red Hat Universal Base Image 10", "platform": "Linux", "dir": "ubi10",
"archs": ["x86_64", "aarch64"]}
]
}
]"#;
fn builds(version: &str) -> Vec<LinuxBuild> {
let releases: Vec<ApiRelease> =
serde_json::from_str(RELEASES_FIXTURE).expect("valid fixture");
releases
.iter()
.find(|release| release.name == version)
.expect("fixture has the release")
.platforms
.iter()
.filter_map(LinuxBuild::from_api)
.collect()
}
fn host(os_release: &str) -> HostDistro {
let release = LinuxOsRelease::parse(os_release).expect("valid os-release");
HostDistro::from_os_release(&release)
.unwrap_or_else(|| HostDistro::unrecognized(os_release_id(&release)))
}
fn chosen(version: &str, os_release: &str, arch: &str) -> String {
select(version, os_release, arch).0
}
fn select(version: &str, os_release: &str, arch: &str) -> (String, Fit) {
let builds = builds(version);
let (build, fit) = select_build(&builds, &host(os_release), arch).expect("a build");
(build.token.clone(), fit)
}
#[test]
fn parses_linux_builds_and_honors_the_dir_override() {
let tokens: Vec<_> = builds("6.3.3").iter().map(|b| b.token.clone()).collect();
assert_eq!(
tokens,
vec![
"ubuntu22.04",
"ubuntu24.04",
"debian12",
"fedora39",
"fedora41",
"amazonlinux2",
"ubi9",
]
);
}
#[test]
fn distro_versions_keep_their_spelling_and_still_order() {
assert_eq!(DistroVersion::new("24.04").raw, "24.04");
assert!(DistroVersion::new("24.04") > DistroVersion::new("22.04"));
assert!(DistroVersion::new("2023") > DistroVersion::new("2"));
assert!(DistroVersion::new("9") < DistroVersion::new("10"));
}
#[test]
fn exact_distro_match_wins() {
assert_eq!(
chosen("6.3.3", "ID=ubuntu\nVERSION_ID=\"24.04\"\n", "x86_64"),
"ubuntu24.04"
);
assert_eq!(
chosen("6.3.3", "ID=fedora\nVERSION_ID=39\n", "x86_64"),
"fedora39"
);
}
#[test]
fn unlisted_version_takes_the_newest_older_build() {
assert_eq!(
chosen("6.3.3", "ID=fedora\nVERSION_ID=40\n", "x86_64"),
"fedora39"
);
assert_eq!(
chosen("6.3.3", "ID=ubuntu\nVERSION_ID=\"25.10\"\n", "x86_64"),
"ubuntu24.04"
);
}
#[test]
fn host_older_than_every_build_takes_the_oldest() {
assert_eq!(
chosen("6.3.3", "ID=ubuntu\nVERSION_ID=\"20.04\"\n", "x86_64"),
"ubuntu22.04"
);
}
#[test]
fn unknown_distro_falls_back_instead_of_fabricating_a_name() {
assert_eq!(
chosen(
"6.3.3",
"ID=omarchy\nID_LIKE=arch\nVERSION_ID=4.0.1rc2\n",
"aarch64"
),
"ubi9"
);
assert_eq!(chosen("6.3.3", "ID=arch\n", "x86_64"), "ubi9");
}
#[test]
fn derivatives_use_their_base_family() {
assert_eq!(
chosen(
"6.3.3",
"ID=linuxmint\nID_LIKE=\"ubuntu debian\"\nVERSION_ID=22\n",
"x86_64"
),
"ubuntu22.04"
);
}
#[test]
fn rhel_rebuilds_match_on_the_major_version() {
assert_eq!(
chosen("6.3.3", "ID=rocky\nVERSION_ID=\"9.4\"\n", "x86_64"),
"ubi9"
);
assert_eq!(host("ID=rocky\nVERSION_ID=\"9.4\"\n").label(), "ubi9");
}
#[test]
fn a_family_without_the_architecture_falls_through() {
assert_eq!(
chosen("6.4.0", "ID=fedora\nVERSION_ID=41\n", "x86_64"),
"fedora41"
);
assert_eq!(
chosen("6.4.0", "ID=fedora\nVERSION_ID=41\n", "aarch64"),
"ubi9"
);
}
#[test]
fn selection_follows_the_release_rather_than_a_fixed_map() {
assert_eq!(
chosen("6.3.3", "ID=amzn\nVERSION_ID=2\n", "x86_64"),
"amazonlinux2"
);
assert_eq!(
chosen("6.4.0", "ID=fedora\nVERSION_ID=39\n", "x86_64"),
"fedora41"
);
}
#[test]
fn a_host_older_than_every_build_is_flagged() {
assert_eq!(
select("6.4.0", "ID=amzn\nVERSION_ID=2\n", "x86_64"),
("amazonlinux2023".to_string(), Fit::NewerThanHost)
);
assert_eq!(
select("6.3.3", "ID=ubuntu\nVERSION_ID=\"20.04\"\n", "x86_64"),
("ubuntu22.04".to_string(), Fit::NewerThanHost)
);
}
#[test]
fn fit_distinguishes_the_kind_of_compromise() {
assert_eq!(
select("6.3.3", "ID=ubuntu\nVERSION_ID=\"24.04\"\n", "x86_64").1,
Fit::Exact
);
assert_eq!(
select("6.3.3", "ID=fedora\nVERSION_ID=40\n", "x86_64").1,
Fit::OlderThanHost
);
assert_eq!(select("6.3.3", "ID=arch\n", "x86_64").1, Fit::OtherFamily);
assert_eq!(
select("6.4.0", "ID=fedora\nVERSION_ID=41\n", "aarch64").1,
Fit::OtherFamily
);
}
#[test]
fn an_unsupported_architecture_selects_nothing() {
for arch in ["riscv64", "loongarch64", "x86"] {
assert!(
select_build(
&builds("6.3.3"),
&host("ID=ubuntu\nVERSION_ID=\"24.04\"\n"),
arch
)
.is_none(),
"{arch} should select nothing"
);
}
}
#[test]
fn a_guessed_base_version_is_not_reported_as_exact() {
assert_eq!(
select(
"6.3.3",
"ID=linuxmint\nID_LIKE=\"ubuntu debian\"\nVERSION_ID=22\n",
"x86_64"
),
("ubuntu22.04".to_string(), Fit::UnknownVersion)
);
}
#[test]
fn host_labels_describe_the_machine() {
assert_eq!(
host("ID=ubuntu\nVERSION_ID=\"24.04\"\n").label(),
"ubuntu24.04"
);
assert_eq!(host("ID=fedora\nVERSION_ID=40\n").label(), "fedora40");
assert_eq!(host("ID=omarchy\nID_LIKE=arch\n").label(), "ubi");
}
}
#[cfg(test)]
mod lockfile_tests {
use super::*;
use crate::config::settings::SettingsPartial;
use crate::platform::Platform;
use crate::toolset::ToolSource;
use confique::Layer;
static TEST_SETTINGS_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
struct SettingsResetGuard {
_lock: std::sync::MutexGuard<'static, ()>,
}
impl Drop for SettingsResetGuard {
fn drop(&mut self) {
Settings::reset(None);
}
}
fn pin_platform(platform: Option<&str>) -> SettingsResetGuard {
let lock = crate::test::lock_ignoring_poison(&TEST_SETTINGS_LOCK);
let guard = SettingsResetGuard { _lock: lock };
let mut settings = SettingsPartial::empty();
settings.swift.platform = platform.map(str::to_string);
Settings::reset(Some(settings));
guard
}
fn target(platform: &str) -> PlatformTarget {
PlatformTarget::new(Platform::parse(platform).expect("valid platform"))
}
fn tool_version(backend: &SwiftPlugin, version: &str) -> ToolVersion {
let request = ToolRequest::new(backend.ba().clone(), version, ToolSource::Unknown)
.expect("valid swift request");
ToolVersion::new(request, version.to_string())
}
fn options(
backend: &SwiftPlugin,
tv: &ToolVersion,
platform: &str,
) -> BTreeMap<String, String> {
backend
.resolve_lockfile_options(&tv.request, &target(platform))
.expect("swift lockfile options")
}
#[test]
fn lockfile_options_record_the_pinned_distro() {
let _guard = pin_platform(Some("ubi9"));
let backend = SwiftPlugin::new();
let tv = tool_version(&backend, "6.3.1");
assert_eq!(
options(&backend, &tv, "linux-x64"),
BTreeMap::from([("swift_platform".to_string(), "ubi9".to_string())])
);
}
#[test]
fn lockfile_options_differ_between_distros() {
let ubuntu = {
let _guard = pin_platform(Some("ubuntu24.04"));
let backend = SwiftPlugin::new();
let tv = tool_version(&backend, "6.3.1");
options(&backend, &tv, "linux-x64")
};
let fedora = {
let _guard = pin_platform(Some("fedora39"));
let backend = SwiftPlugin::new();
let tv = tool_version(&backend, "6.3.1");
options(&backend, &tv, "linux-x64")
};
assert_ne!(ubuntu, fedora);
}
#[test]
fn lockfile_options_are_empty_off_linux() {
let _guard = pin_platform(None);
let backend = SwiftPlugin::new();
let tv = tool_version(&backend, "6.3.1");
assert!(options(&backend, &tv, "macos-arm64").is_empty());
assert!(options(&backend, &tv, "windows-x64").is_empty());
}
#[test]
fn url_is_built_for_the_target_platform() {
let _guard = pin_platform(Some("ubuntu24.04"));
let backend = SwiftPlugin::new();
let tv = tool_version(&backend, "6.3.1");
assert_eq!(
url(&tv, &target("linux-arm64"), "ubuntu24.04"),
"https://download.swift.org/swift-6.3.1-release/ubuntu2404-aarch64/swift-6.3.1-RELEASE/swift-6.3.1-RELEASE-ubuntu24.04-aarch64.tar.gz"
);
}
#[test]
fn arm64_urls_use_the_arch_directory_on_every_distro() {
for (pinned, directory, filename) in [
("ubuntu24.04", "ubuntu2404-aarch64", "ubuntu24.04-aarch64"),
("ubi9", "ubi9-aarch64", "ubi9-aarch64"),
("fedora39", "fedora39-aarch64", "fedora39-aarch64"),
(
"amazonlinux2",
"amazonlinux2-aarch64",
"amazonlinux2-aarch64",
),
] {
let _guard = pin_platform(Some(pinned));
let backend = SwiftPlugin::new();
let tv = tool_version(&backend, "6.3.3");
assert_eq!(
url(&tv, &target("linux-arm64"), pinned),
format!(
"https://download.swift.org/swift-6.3.3-release/{directory}/swift-6.3.3-RELEASE/swift-6.3.3-RELEASE-{filename}.tar.gz"
)
);
}
}
#[test]
fn x64_urls_have_no_arch_suffix() {
let _guard = pin_platform(Some("ubi9"));
let backend = SwiftPlugin::new();
let tv = tool_version(&backend, "6.3.3");
assert_eq!(
url(&tv, &target("linux-x64"), "ubi9"),
"https://download.swift.org/swift-6.3.3-release/ubi9/swift-6.3.3-RELEASE/swift-6.3.3-RELEASE-ubi9.tar.gz"
);
}
#[test]
fn windows_arm64_uses_the_arch_directory() {
let _guard = pin_platform(None);
let backend = SwiftPlugin::new();
let tv = tool_version(&backend, "6.3.3");
assert_eq!(
url(&tv, &target("windows-arm64"), "windows10"),
"https://download.swift.org/swift-6.3.3-release/windows10-arm64/swift-6.3.3-RELEASE/swift-6.3.3-RELEASE-windows10-arm64.exe"
);
}
#[tokio::test]
async fn pinned_distro_is_ignored_off_linux() {
let _guard = pin_platform(Some("ubi9"));
let backend = SwiftPlugin::new();
let tv = tool_version(&backend, "6.3.1");
assert_eq!(
resolve_platform(&tv, &target("macos-arm64")).await.unwrap(),
"osx"
);
assert_eq!(
resolve_platform(&tv, &target("windows-x64")).await.unwrap(),
"windows10"
);
}
#[test]
fn pinned_distro_does_not_apply_off_linux() {
let _guard = pin_platform(Some("ubi9"));
let backend = SwiftPlugin::new();
let tv = tool_version(&backend, "6.3.1");
assert_eq!(
url(&tv, &target("macos-arm64"), "osx"),
"https://download.swift.org/swift-6.3.1-release/xcode/swift-6.3.1-RELEASE/swift-6.3.1-RELEASE-osx.pkg"
);
assert_eq!(
url(&tv, &target("windows-x64"), "windows10"),
"https://download.swift.org/swift-6.3.1-release/windows10/swift-6.3.1-RELEASE/swift-6.3.1-RELEASE-windows10.exe"
);
}
#[tokio::test]
async fn musl_targets_are_refused_before_any_build_is_chosen() {
let _guard = pin_platform(None);
let backend = SwiftPlugin::new();
let tv = tool_version(&backend, "6.3.1");
assert!(
resolve_platform(&tv, &target("linux-x64-musl"))
.await
.is_err()
);
}
#[tokio::test]
async fn a_pinned_platform_resolves_without_the_release_index() {
let _guard = pin_platform(Some("ubi9"));
let backend = SwiftPlugin::new();
let tv = tool_version(&backend, "0.0.0-not-a-release");
assert_eq!(
resolve_platform(&tv, &target("linux-x64")).await.unwrap(),
"ubi9"
);
}
#[tokio::test]
async fn a_platform_pin_does_not_override_the_musl_refusal() {
let _guard = pin_platform(Some("ubuntu24.04"));
let backend = SwiftPlugin::new();
let tv = tool_version(&backend, "6.3.1");
assert!(
resolve_platform(&tv, &target("linux-x64-musl"))
.await
.is_err()
);
assert_eq!(
resolve_platform(&tv, &target("linux-x64")).await.unwrap(),
"ubuntu24.04"
);
}
#[tokio::test]
async fn musl_targets_have_nothing_to_lock() {
let _guard = pin_platform(None);
let backend = SwiftPlugin::new();
let tv = tool_version(&backend, "6.3.1");
assert!(
backend
.resolve_lock_info(&tv, &target("linux-x64-musl"))
.await
.is_err()
);
}
#[test]
fn foreign_linux_targets_fall_back_to_ubuntu() {
let _guard = pin_platform(None);
let backend = SwiftPlugin::new();
let tv = tool_version(&backend, "6.3.1");
let foreign = target("linux-riscv64");
assert!(!foreign.is_current());
assert_eq!(
options(&backend, &tv, "linux-riscv64"),
BTreeMap::from([(
"swift_platform".to_string(),
format!("ubuntu{DEFAULT_UBUNTU_VERSION}")
)])
);
assert_eq!(
url(&tv, &foreign, "ubuntu24.04"),
"https://download.swift.org/swift-6.3.1-release/ubuntu2404-riscv64/swift-6.3.1-RELEASE/swift-6.3.1-RELEASE-ubuntu24.04-riscv64.tar.gz"
);
}
#[test]
fn every_suffixed_architecture_gets_a_matching_directory() {
let _guard = pin_platform(Some("ubuntu24.04"));
let backend = SwiftPlugin::new();
let tv = tool_version(&backend, "6.3.1");
for (platform, arch) in [
("linux-x86", "x86"),
("linux-riscv64", "riscv64"),
("linux-loongarch64", "loongarch64"),
] {
assert_eq!(
url(&tv, &target(platform), "ubuntu24.04"),
format!(
"https://download.swift.org/swift-6.3.1-release/ubuntu2404-{arch}/swift-6.3.1-RELEASE/swift-6.3.1-RELEASE-ubuntu24.04-{arch}.tar.gz"
)
);
}
}
}
#[cfg(all(test, target_os = "linux"))]
mod linux_tests {
use super::{missing_sonames, parse_ldd_missing};
use crate::file;
#[test]
fn parse_ldd_missing_reads_unresolved_sonames() {
let output = "\tlinux-vdso.so.1 (0x0000ffff123)\n\
\tlibncurses.so.6 => not found\n\
\tlibc.so.6 => /usr/lib/libc.so.6 (0x0000ffff456)\n\
\tlibpanel.so.6 => not found\n";
assert_eq!(
parse_ldd_missing(output),
vec!["libncurses.so.6".to_string(), "libpanel.so.6".to_string()]
);
}
#[test]
fn parse_ldd_missing_ignores_a_file_that_is_not_dynamic() {
assert!(parse_ldd_missing("\tnot a dynamic executable\n").is_empty());
}
#[test]
fn missing_sonames_tolerates_files_that_are_not_dynamic_executables() {
let tmp = tempfile::tempdir().unwrap();
let bin = tmp.path().join("usr/bin");
let lib = tmp.path().join("usr/lib");
file::create_dir_all(&bin).unwrap();
file::create_dir_all(&lib).unwrap();
file::write(bin.join("swift-helper"), "#!/bin/sh\nexit 0\n").unwrap();
file::make_executable(bin.join("swift-helper")).unwrap();
file::write(lib.join("libnotreally.so.1"), "not an ELF").unwrap();
file::write(lib.join("swift.json"), "{}").unwrap();
assert!(missing_sonames(tmp.path(), &Default::default()).is_empty());
}
#[test]
fn missing_sonames_is_empty_without_a_toolchain_layout() {
let tmp = tempfile::tempdir().unwrap();
assert!(missing_sonames(tmp.path(), &Default::default()).is_empty());
}
}