use {
crate::{project_layout::PyembedLocation, py_packaging::distribution::AppleSdkInfo},
anyhow::{anyhow, Context, Result},
apple_sdk::{AppleSdk, ParsedSdk, SdkSearch, SdkSearchLocation, SdkSorting},
log::{info, warn},
once_cell::sync::Lazy,
std::{
env,
ops::Deref,
path::{Path, PathBuf},
sync::{Arc, RwLock},
},
tugger_rust_toolchain::install_rust_toolchain,
};
const PYOXIDIZER_CRATE_VERSION: &str = env!("CARGO_PKG_VERSION");
const PYEMBED_CRATE_VERSION: &str = "0.24.0";
const GIT_REPO_URL: &str = env!("GIT_REPO_URL");
pub const PYOXIDIZER_VERSION: &str = env!("PYOXIDIZER_VERSION");
pub static BUILD_GIT_REPO_PATH: Lazy<Option<PathBuf>> = Lazy::new(|| {
match env!("GIT_REPO_PATH") {
"" => None,
value => {
let path = PathBuf::from(value);
if path.exists() {
Some(path)
} else {
None
}
}
}
});
pub static BUILD_GIT_COMMIT: Lazy<Option<String>> = Lazy::new(|| {
match env!("GIT_COMMIT") {
"" => None,
value => Some(value.to_string()),
}
});
pub static BUILD_GIT_TAG: Lazy<Option<String>> = Lazy::new(|| {
let tag = env!("GIT_TAG");
if tag.is_empty() {
None
} else {
Some(tag.to_string())
}
});
pub static GIT_SOURCE: Lazy<PyOxidizerSource> = Lazy::new(|| {
let commit = BUILD_GIT_COMMIT.clone();
let tag = if commit.is_some() || BUILD_GIT_TAG.is_none() {
None
} else {
BUILD_GIT_TAG.clone()
};
PyOxidizerSource::GitUrl {
url: GIT_REPO_URL.to_owned(),
commit,
tag,
}
});
pub static MINIMUM_RUST_VERSION: Lazy<semver::Version> =
Lazy::new(|| semver::Version::new(1, 62, 1));
pub const RUST_TOOLCHAIN_VERSION: &str = "1.66.0";
pub static LINUX_TARGET_TRIPLES: Lazy<Vec<&'static str>> = Lazy::new(|| {
vec![
"aarch64-unknown-linux-gnu",
"x86_64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
]
});
pub static MACOS_TARGET_TRIPLES: Lazy<Vec<&'static str>> =
Lazy::new(|| vec!["aarch64-apple-darwin", "x86_64-apple-darwin"]);
pub static WINDOWS_TARGET_TRIPLES: Lazy<Vec<&'static str>> = Lazy::new(|| {
vec![
"i686-pc-windows-gnu",
"i686-pc-windows-msvc",
"x86_64-pc-windows-gnu",
"x86_64-pc-windows-msvc",
]
});
pub fn canonicalize_path(path: &Path) -> Result<PathBuf, std::io::Error> {
let mut p = path.canonicalize()?;
if cfg!(windows) {
let mut s = p.display().to_string().replace('\\', "/");
if s.starts_with("//?/") {
s = s[4..].to_string();
}
p = PathBuf::from(s);
}
Ok(p)
}
pub fn default_target_triple() -> &'static str {
match env!("TARGET") {
"aarch64-unknown-linux-musl" => "aarch64-unknown-linux-gnu",
"x86_64-unknown-linux-musl" => "x86_64-unknown-linux-gnu",
v => v,
}
}
#[derive(Clone, Debug)]
pub enum PyOxidizerSource {
LocalPath { path: PathBuf },
GitUrl {
url: String,
commit: Option<String>,
tag: Option<String>,
},
}
impl Default for PyOxidizerSource {
fn default() -> Self {
if let Some(path) = BUILD_GIT_REPO_PATH.as_ref() {
Self::LocalPath { path: path.clone() }
} else {
GIT_SOURCE.clone()
}
}
}
impl PyOxidizerSource {
pub fn as_pyembed_location(&self) -> PyembedLocation {
if PYEMBED_CRATE_VERSION.ends_with("-pre") {
match self {
PyOxidizerSource::LocalPath { path } => {
PyembedLocation::Path(canonicalize_path(&path.join("pyembed")).unwrap())
}
PyOxidizerSource::GitUrl { url, commit, tag } => {
if let Some(tag) = tag {
PyembedLocation::Git(url.clone(), tag.clone())
} else if let Some(commit) = commit {
PyembedLocation::Git(url.clone(), commit.clone())
} else {
PyembedLocation::Git(url.clone(), "main".to_string())
}
}
}
} else {
PyembedLocation::Version(PYEMBED_CRATE_VERSION.to_string())
}
}
pub fn version_long(&self) -> String {
format!(
"{}\ncommit: {}\nsource: {}\npyembed crate location: {}",
PYOXIDIZER_CRATE_VERSION,
if let Some(commit) = BUILD_GIT_COMMIT.as_ref() {
commit.as_str()
} else {
"unknown"
},
match self {
PyOxidizerSource::LocalPath { path } => {
format!("{}", path.display())
}
PyOxidizerSource::GitUrl { url, .. } => {
url.clone()
}
},
self.as_pyembed_location().cargo_manifest_fields(),
)
}
}
fn cargo_target_directory() -> Result<Option<PathBuf>> {
if std::env::var_os("CARGO_MANIFEST_DIR").is_none() {
return Ok(None);
}
let mut exe = std::env::current_exe().context("locating current executable")?;
exe.pop();
if exe.ends_with("deps") {
exe.pop();
}
Ok(Some(exe))
}
#[derive(Clone, Debug)]
pub struct Environment {
pub pyoxidizer_source: PyOxidizerSource,
cargo_target_directory: Option<PathBuf>,
cache_dir: PathBuf,
managed_rust: bool,
rust_environment: Arc<RwLock<Option<RustEnvironment>>>,
}
impl Environment {
pub fn new() -> Result<Self> {
let pyoxidizer_source = PyOxidizerSource::default();
let cache_dir = if let Ok(p) = std::env::var("PYOXIDIZER_CACHE_DIR") {
PathBuf::from(p)
} else if let Some(cache_dir) = dirs::cache_dir() {
cache_dir.join("pyoxidizer")
} else {
dirs::home_dir().ok_or_else(|| anyhow!("could not resolve home dir as part of resolving PyOxidizer cache directory"))?.join(".pyoxidizer").join("cache")
};
let managed_rust = std::env::var("PYOXIDIZER_SYSTEM_RUST").is_err();
Ok(Self {
pyoxidizer_source,
cargo_target_directory: cargo_target_directory()?,
cache_dir,
managed_rust,
rust_environment: Arc::new(RwLock::new(None)),
})
}
pub fn cache_dir(&self) -> &Path {
&self.cache_dir
}
pub fn python_distributions_dir(&self) -> PathBuf {
self.cache_dir.join("python_distributions")
}
pub fn rust_dir(&self) -> PathBuf {
self.cache_dir.join("rust")
}
pub fn unmanage_rust(&mut self) -> Result<()> {
self.managed_rust = false;
self.rust_environment
.write()
.map_err(|e| anyhow!("unable to lock cached rust environment for writing: {}", e))?
.take();
Ok(())
}
pub fn find_executable(&self, name: &str) -> which::Result<Option<PathBuf>> {
match which::which(name) {
Ok(p) => Ok(Some(p)),
Err(which::Error::CannotFindBinaryPath) => Ok(None),
Err(e) => Err(e),
}
}
pub fn ensure_rust_toolchain(&self, target_triple: Option<&str>) -> Result<RustEnvironment> {
let mut cached = self
.rust_environment
.write()
.map_err(|e| anyhow!("failed to acquire rust environment lock: {}", e))?;
if cached.is_none() {
warn!(
"ensuring Rust toolchain {} is available",
RUST_TOOLCHAIN_VERSION,
);
let rust_env = if self.managed_rust {
#[allow(clippy::redundant_closure)]
let target_triple = target_triple.unwrap_or_else(|| default_target_triple());
let toolchain = install_rust_toolchain(
RUST_TOOLCHAIN_VERSION,
default_target_triple(),
&[target_triple],
&self.rust_dir(),
Some(&self.rust_dir()),
)?;
RustEnvironment {
cargo_exe: toolchain.cargo_path,
rustc_exe: toolchain.rustc_path.clone(),
rust_version: rustc_version::VersionMeta::for_command(
std::process::Command::new(toolchain.rustc_path),
)?,
}
} else {
self.system_rust_environment()?
};
cached.replace(rust_env);
}
Ok(cached
.deref()
.as_ref()
.expect("should have been populated above")
.clone())
}
fn rustc_exe(&self) -> which::Result<Option<PathBuf>> {
if let Some(v) = std::env::var_os("RUSTC") {
let p = PathBuf::from(v);
if p.exists() {
Ok(Some(p))
} else {
Err(which::Error::BadAbsolutePath)
}
} else {
self.find_executable("rustc")
}
}
fn cargo_exe(&self) -> which::Result<Option<PathBuf>> {
self.find_executable("cargo")
}
fn system_rust_environment(&self) -> Result<RustEnvironment> {
let cargo_exe = self
.cargo_exe()
.context("finding cargo executable")?
.ok_or_else(|| anyhow!("cargo executable not found; is Rust installed and in PATH?"))?;
let rustc_exe = self
.rustc_exe()
.context("finding rustc executable")?
.ok_or_else(|| anyhow!("rustc executable not found; is Rust installed and in PATH?"))?;
let rust_version =
rustc_version::VersionMeta::for_command(std::process::Command::new(&rustc_exe))
.context("resolving rustc version")?;
if rust_version.semver.lt(&MINIMUM_RUST_VERSION) {
return Err(anyhow!(
"PyOxidizer requires Rust {}; {} is version {}",
*MINIMUM_RUST_VERSION,
rustc_exe.display(),
rust_version.semver
));
}
Ok(RustEnvironment {
cargo_exe,
rustc_exe,
rust_version,
})
}
pub fn resolve_apple_sdk(&self, sdk_info: &AppleSdkInfo) -> Result<ParsedSdk> {
let platform = &sdk_info.platform;
let minimum_version = &sdk_info.version;
let deployment_target = &sdk_info.deployment_target;
warn!(
"locating Apple SDK {}{}+ supporting {}{}",
platform, minimum_version, platform, deployment_target
);
let sdks = SdkSearch::default()
.progress_callback(|event| {
info!("{}", event);
})
.location(SdkSearchLocation::SystemXcodes)
.platform(platform.as_str().try_into()?)
.minimum_version(minimum_version)
.deployment_target(platform, deployment_target)
.sorting(SdkSorting::VersionDescending)
.search::<ParsedSdk>()?;
if sdks.is_empty() {
return Err(anyhow!(
"unable to find suitable Apple SDK supporting {}{} or newer",
platform,
minimum_version
));
}
let sdk = sdks.into_iter().next().unwrap();
if sdk
.version()
.expect("ParsedSDK should always have version")
.clone()
< minimum_version.as_str().into()
{
warn!(
"WARNING: SDK does not meet minimum version requirement of {}; build errors or unexpected behavior may occur",
minimum_version
);
}
warn!(
"using {} targeting {}{}",
sdk.sdk_path(),
platform,
deployment_target
);
Ok(sdk)
}
pub fn temporary_directory(&self, prefix: &str) -> Result<tempfile::TempDir> {
let mut builder = tempfile::Builder::new();
builder.prefix(prefix);
if let Some(target_dir) = &self.cargo_target_directory {
let base = target_dir.join("tempdir");
std::fs::create_dir_all(&base)
.context("creating temporary directory base in cargo target dir")?;
builder.tempdir_in(&base)
} else {
builder.tempdir()
}
.context("creating temporary directory")
}
}
#[derive(Clone, Debug)]
pub struct RustEnvironment {
pub cargo_exe: PathBuf,
pub rustc_exe: PathBuf,
pub rust_version: rustc_version::VersionMeta,
}