use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{Context, Result};
use prek_consts::env_vars::{EnvVars, EnvVarsRead};
use prek_consts::prepend_paths;
use semver::Version;
use tracing::debug;
use crate::cli::reporter::HookInstallReporter;
use crate::git::GitCommandExt;
use crate::hook::{Hook, InstallInfo, InstalledHook};
use crate::languages::{ExecutionEnvironment, LanguageBackend};
use crate::process::Cmd;
use crate::store::Store;
#[derive(Debug, Copy, Clone)]
pub(crate) struct Swift;
pub(crate) struct SwiftInfo {
pub(crate) version: Version,
pub(crate) executable: PathBuf,
}
pub(crate) async fn query_swift_info() -> Result<SwiftInfo> {
let executable = which::which("swift").context("Swift not found on PATH")?;
let stdout = Cmd::new("swift")
.arg("--version")
.check(true)
.output()
.await?
.stdout;
let output = str::from_utf8(&stdout)?;
let version = parse_swift_version(output).context("Failed to parse Swift version")?;
Ok(SwiftInfo {
version,
executable,
})
}
fn normalize_version(version_str: &str) -> String {
let version_str = version_str.split('-').next().unwrap_or(version_str);
if version_str.matches('.').count() == 1 {
format!("{version_str}.0")
} else {
version_str.to_string()
}
}
fn parse_swift_version(output: &str) -> Option<Version> {
for line in output.lines() {
if let Some(idx) = line.find("Apple Swift version ") {
let rest = &line[idx + "Apple Swift version ".len()..];
if let Some(version_str) = rest.split_whitespace().next() {
if let Ok(version) = normalize_version(version_str).parse() {
return Some(version);
}
}
}
if let Some(rest) = line.strip_prefix("Swift version ") {
let version_str = rest.split_whitespace().next()?;
return normalize_version(version_str).parse().ok();
}
}
None
}
fn build_dir(env_path: &Path) -> PathBuf {
env_path.join(".build")
}
const BIN_PATH_KEY: &str = "swift_bin_path";
#[async_trait::async_trait(?Send)]
impl LanguageBackend for Swift {
async fn install(
&self,
store: &Store,
hook: Arc<Hook>,
install_cwd: &Path,
reporter: &HookInstallReporter,
) -> Result<InstalledHook> {
let progress = reporter.on_install_start(&hook);
let mut info = InstallInfo::new(&hook, &store.hooks_dir())?;
debug!(%hook, target = %info.env_path.display(), "Installing Swift environment");
let swift_info = query_swift_info()
.await
.context("Failed to query Swift info")?;
if let Some(repo_path) = hook.repo_path() {
if repo_path.join("Package.swift").exists() {
debug!(%hook, "Building Swift package");
let build_path = build_dir(&info.env_path);
Cmd::new("swift")
.current_dir(install_cwd)
.arg("build")
.arg("-c")
.arg("release")
.arg("--package-path")
.arg(repo_path)
.arg("--build-path")
.arg(&build_path)
.sanitize_git_repo_env()
.check(true)
.output()
.await
.context("Failed to build Swift package")?;
let bin_path_output = Cmd::new("swift")
.current_dir(install_cwd)
.arg("build")
.arg("-c")
.arg("release")
.arg("--package-path")
.arg(repo_path)
.arg("--build-path")
.arg(&build_path)
.arg("--show-bin-path")
.sanitize_git_repo_env()
.check(true)
.output()
.await
.context("Failed to get Swift bin path")?;
let bin_path = String::from_utf8_lossy(&bin_path_output.stdout)
.trim()
.to_string();
debug!(%hook, %bin_path, "Swift bin path");
info.with_extra(BIN_PATH_KEY, &bin_path);
} else {
debug!(%hook, "No Package.swift found, skipping build");
}
}
info.with_toolchain(swift_info.executable)
.with_language_version(swift_info.version);
info.persist_env_path();
reporter.on_install_complete(progress);
Ok(InstalledHook::Installed {
hook,
info: Arc::new(info),
})
}
async fn check_health(&self, info: &InstallInfo) -> Result<()> {
if !info.toolchain.exists() {
anyhow::bail!(
"Swift executable no longer exists at: {}",
info.toolchain.display()
);
}
Ok(())
}
fn execution_environment(
&self,
_store: &Store,
hook: &InstalledHook,
) -> Result<ExecutionEnvironment> {
let new_path =
if let Some(bin_path) = hook.install_info().and_then(|i| i.get_extra(BIN_PATH_KEY)) {
prepend_paths(&[Path::new(bin_path)]).context("Failed to join PATH")?
} else {
EnvVars.var_os(EnvVars::PATH).unwrap_or_default()
};
let mut environment = ExecutionEnvironment::new();
environment.set_path(&new_path);
Ok(environment)
}
}
#[cfg(test)]
mod tests {
use super::parse_swift_version;
#[test]
fn test_parse_macos_format() {
let output = "swift-driver version: 1.115.0 Apple Swift version 6.1.2 (swiftlang-6.1.2.1.1 clang-1700.0.13.1)";
let version = parse_swift_version(output).unwrap();
assert_eq!(version.major, 6);
assert_eq!(version.minor, 1);
assert_eq!(version.patch, 2);
}
#[test]
fn test_parse_linux_format() {
let output = "Swift version 6.1.2 (swift-6.1.2-RELEASE)";
let version = parse_swift_version(output).unwrap();
assert_eq!(version.major, 6);
assert_eq!(version.minor, 1);
assert_eq!(version.patch, 2);
}
#[test]
fn test_parse_multiline_output() {
let output = r"swift-driver version: 1.115.0 Apple Swift version 6.1.2 (swiftlang-6.1.2.1.1 clang-1700.0.13.1)
Target: arm64-apple-macosx15.0";
let version = parse_swift_version(output).unwrap();
assert_eq!(version.major, 6);
assert_eq!(version.minor, 1);
assert_eq!(version.patch, 2);
}
#[test]
fn test_parse_linux_multiline() {
let output = r"Swift version 6.1.2 (swift-6.1.2-RELEASE)
Target: x86_64-unknown-linux-gnu";
let version = parse_swift_version(output).unwrap();
assert_eq!(version.major, 6);
assert_eq!(version.minor, 1);
assert_eq!(version.patch, 2);
}
#[test]
fn test_parse_invalid_output() {
assert!(parse_swift_version("").is_none());
assert!(parse_swift_version("not a version string").is_none());
assert!(parse_swift_version("version 6.1.2").is_none()); }
#[test]
fn test_parse_version_without_patch() {
let output = "swift-driver version: 1.115.0 Apple Swift version 6.1 (swiftlang-6.1.0.0.1 clang-1700.0.13.1)";
let version = parse_swift_version(output).unwrap();
assert_eq!(version.major, 6);
assert_eq!(version.minor, 1);
assert_eq!(version.patch, 0);
let output = "Swift version 6.1 (swift-6.1-RELEASE)";
let version = parse_swift_version(output).unwrap();
assert_eq!(version.major, 6);
assert_eq!(version.minor, 1);
assert_eq!(version.patch, 0);
}
#[test]
fn test_parse_dev_version() {
let output = "Swift version 6.2-dev (LLVM abcdef, Swift 123456)";
let version = parse_swift_version(output).unwrap();
assert_eq!(version.major, 6);
assert_eq!(version.minor, 2);
assert_eq!(version.patch, 0);
}
}