1use {
8 crate::{project_layout::PyembedLocation, py_packaging::distribution::AppleSdkInfo},
9 anyhow::{anyhow, Context, Result},
10 apple_sdk::{AppleSdk, ParsedSdk, SdkSearch, SdkSearchLocation, SdkSorting},
11 log::{info, warn},
12 once_cell::sync::Lazy,
13 std::{
14 env,
15 ops::Deref,
16 path::{Path, PathBuf},
17 sync::{Arc, RwLock},
18 },
19 tugger_rust_toolchain::install_rust_toolchain,
20};
21
22const PYOXIDIZER_CRATE_VERSION: &str = env!("CARGO_PKG_VERSION");
24
25const PYEMBED_CRATE_VERSION: &str = "0.24.0";
27
28const GIT_REPO_URL: &str = env!("GIT_REPO_URL");
30
31pub const PYOXIDIZER_VERSION: &str = env!("PYOXIDIZER_VERSION");
33
34pub static BUILD_GIT_REPO_PATH: Lazy<Option<PathBuf>> = Lazy::new(|| {
38 match env!("GIT_REPO_PATH") {
39 "" => None,
40 value => {
41 let path = PathBuf::from(value);
42
43 if path.exists() {
46 Some(path)
47 } else {
48 None
49 }
50 }
51 }
52});
53
54pub static BUILD_GIT_COMMIT: Lazy<Option<String>> = Lazy::new(|| {
56 match env!("GIT_COMMIT") {
57 "" => None,
60 value => Some(value.to_string()),
61 }
62});
63
64pub static BUILD_GIT_TAG: Lazy<Option<String>> = Lazy::new(|| {
66 let tag = env!("GIT_TAG");
67 if tag.is_empty() {
68 None
69 } else {
70 Some(tag.to_string())
71 }
72});
73
74pub static GIT_SOURCE: Lazy<PyOxidizerSource> = Lazy::new(|| {
76 let commit = BUILD_GIT_COMMIT.clone();
77
78 let tag = if commit.is_some() || BUILD_GIT_TAG.is_none() {
80 None
81 } else {
82 BUILD_GIT_TAG.clone()
83 };
84
85 PyOxidizerSource::GitUrl {
86 url: GIT_REPO_URL.to_owned(),
87 commit,
88 tag,
89 }
90});
91
92pub static MINIMUM_RUST_VERSION: Lazy<semver::Version> =
97 Lazy::new(|| semver::Version::new(1, 62, 1));
98
99pub const RUST_TOOLCHAIN_VERSION: &str = "1.66.0";
101
102pub static LINUX_TARGET_TRIPLES: Lazy<Vec<&'static str>> = Lazy::new(|| {
104 vec![
105 "aarch64-unknown-linux-gnu",
106 "x86_64-unknown-linux-gnu",
107 "x86_64-unknown-linux-musl",
108 ]
109});
110
111pub static MACOS_TARGET_TRIPLES: Lazy<Vec<&'static str>> =
113 Lazy::new(|| vec!["aarch64-apple-darwin", "x86_64-apple-darwin"]);
114
115pub static WINDOWS_TARGET_TRIPLES: Lazy<Vec<&'static str>> = Lazy::new(|| {
117 vec![
118 "i686-pc-windows-gnu",
119 "i686-pc-windows-msvc",
120 "x86_64-pc-windows-gnu",
121 "x86_64-pc-windows-msvc",
122 ]
123});
124
125pub fn canonicalize_path(path: &Path) -> Result<PathBuf, std::io::Error> {
126 let mut p = path.canonicalize()?;
127
128 if cfg!(windows) {
130 let mut s = p.display().to_string().replace('\\', "/");
131 if s.starts_with("//?/") {
132 s = s[4..].to_string();
133 }
134
135 p = PathBuf::from(s);
136 }
137
138 Ok(p)
139}
140
141pub fn default_target_triple() -> &'static str {
146 match env!("TARGET") {
147 "aarch64-unknown-linux-musl" => "aarch64-unknown-linux-gnu",
150 "x86_64-unknown-linux-musl" => "x86_64-unknown-linux-gnu",
151 v => v,
152 }
153}
154
155#[derive(Clone, Debug)]
157pub enum PyOxidizerSource {
158 LocalPath { path: PathBuf },
160
161 GitUrl {
163 url: String,
164 commit: Option<String>,
165 tag: Option<String>,
166 },
167}
168
169impl Default for PyOxidizerSource {
170 fn default() -> Self {
171 if let Some(path) = BUILD_GIT_REPO_PATH.as_ref() {
172 Self::LocalPath { path: path.clone() }
173 } else {
174 GIT_SOURCE.clone()
175 }
176 }
177}
178
179impl PyOxidizerSource {
180 pub fn as_pyembed_location(&self) -> PyembedLocation {
188 if PYEMBED_CRATE_VERSION.ends_with("-pre") {
190 match self {
191 PyOxidizerSource::LocalPath { path } => {
192 PyembedLocation::Path(canonicalize_path(&path.join("pyembed")).unwrap())
193 }
194 PyOxidizerSource::GitUrl { url, commit, tag } => {
195 if let Some(tag) = tag {
196 PyembedLocation::Git(url.clone(), tag.clone())
197 } else if let Some(commit) = commit {
198 PyembedLocation::Git(url.clone(), commit.clone())
199 } else {
200 PyembedLocation::Git(url.clone(), "main".to_string())
202 }
203 }
204 }
205 } else {
206 PyembedLocation::Version(PYEMBED_CRATE_VERSION.to_string())
208 }
209 }
210
211 pub fn version_long(&self) -> String {
213 format!(
214 "{}\ncommit: {}\nsource: {}\npyembed crate location: {}",
215 PYOXIDIZER_CRATE_VERSION,
216 if let Some(commit) = BUILD_GIT_COMMIT.as_ref() {
217 commit.as_str()
218 } else {
219 "unknown"
220 },
221 match self {
222 PyOxidizerSource::LocalPath { path } => {
223 format!("{}", path.display())
224 }
225 PyOxidizerSource::GitUrl { url, .. } => {
226 url.clone()
227 }
228 },
229 self.as_pyembed_location().cargo_manifest_fields(),
230 )
231 }
232}
233
234fn cargo_target_directory() -> Result<Option<PathBuf>> {
238 if std::env::var_os("CARGO_MANIFEST_DIR").is_none() {
239 return Ok(None);
240 }
241
242 let mut exe = std::env::current_exe().context("locating current executable")?;
243
244 exe.pop();
245 if exe.ends_with("deps") {
246 exe.pop();
247 }
248
249 Ok(Some(exe))
250}
251
252#[derive(Clone, Debug)]
254pub struct Environment {
255 pub pyoxidizer_source: PyOxidizerSource,
257
258 cargo_target_directory: Option<PathBuf>,
263
264 cache_dir: PathBuf,
266
267 managed_rust: bool,
269
270 rust_environment: Arc<RwLock<Option<RustEnvironment>>>,
274}
275
276impl Environment {
277 pub fn new() -> Result<Self> {
279 let pyoxidizer_source = PyOxidizerSource::default();
280
281 let cache_dir = if let Ok(p) = std::env::var("PYOXIDIZER_CACHE_DIR") {
282 PathBuf::from(p)
283 } else if let Some(cache_dir) = dirs::cache_dir() {
284 cache_dir.join("pyoxidizer")
285 } else {
286 dirs::home_dir().ok_or_else(|| anyhow!("could not resolve home dir as part of resolving PyOxidizer cache directory"))?.join(".pyoxidizer").join("cache")
287 };
288
289 let managed_rust = std::env::var("PYOXIDIZER_SYSTEM_RUST").is_err();
290
291 Ok(Self {
292 pyoxidizer_source,
293 cargo_target_directory: cargo_target_directory()?,
294 cache_dir,
295 managed_rust,
296 rust_environment: Arc::new(RwLock::new(None)),
297 })
298 }
299
300 pub fn cache_dir(&self) -> &Path {
304 &self.cache_dir
305 }
306
307 pub fn python_distributions_dir(&self) -> PathBuf {
309 self.cache_dir.join("python_distributions")
310 }
311
312 pub fn rust_dir(&self) -> PathBuf {
314 self.cache_dir.join("rust")
315 }
316
317 pub fn unmanage_rust(&mut self) -> Result<()> {
322 self.managed_rust = false;
323 self.rust_environment
324 .write()
325 .map_err(|e| anyhow!("unable to lock cached rust environment for writing: {}", e))?
326 .take();
327
328 Ok(())
329 }
330
331 pub fn find_executable(&self, name: &str) -> which::Result<Option<PathBuf>> {
337 match which::which(name) {
338 Ok(p) => Ok(Some(p)),
339 Err(which::Error::CannotFindBinaryPath) => Ok(None),
340 Err(e) => Err(e),
341 }
342 }
343
344 pub fn ensure_rust_toolchain(&self, target_triple: Option<&str>) -> Result<RustEnvironment> {
346 let mut cached = self
347 .rust_environment
348 .write()
349 .map_err(|e| anyhow!("failed to acquire rust environment lock: {}", e))?;
350
351 if cached.is_none() {
352 warn!(
353 "ensuring Rust toolchain {} is available",
354 RUST_TOOLCHAIN_VERSION,
355 );
356
357 let rust_env = if self.managed_rust {
358 #[allow(clippy::redundant_closure)]
360 let target_triple = target_triple.unwrap_or_else(|| default_target_triple());
361
362 let toolchain = install_rust_toolchain(
363 RUST_TOOLCHAIN_VERSION,
364 default_target_triple(),
365 &[target_triple],
366 &self.rust_dir(),
367 Some(&self.rust_dir()),
368 )?;
369
370 RustEnvironment {
371 cargo_exe: toolchain.cargo_path,
372 rustc_exe: toolchain.rustc_path.clone(),
373 rust_version: rustc_version::VersionMeta::for_command(
374 std::process::Command::new(toolchain.rustc_path),
375 )?,
376 }
377 } else {
378 self.system_rust_environment()?
379 };
380
381 cached.replace(rust_env);
382 }
383
384 Ok(cached
385 .deref()
386 .as_ref()
387 .expect("should have been populated above")
388 .clone())
389 }
390
391 fn rustc_exe(&self) -> which::Result<Option<PathBuf>> {
398 if let Some(v) = std::env::var_os("RUSTC") {
399 let p = PathBuf::from(v);
400
401 if p.exists() {
402 Ok(Some(p))
403 } else {
404 Err(which::Error::BadAbsolutePath)
405 }
406 } else {
407 self.find_executable("rustc")
408 }
409 }
410
411 fn cargo_exe(&self) -> which::Result<Option<PathBuf>> {
416 self.find_executable("cargo")
417 }
418
419 fn system_rust_environment(&self) -> Result<RustEnvironment> {
425 let cargo_exe = self
426 .cargo_exe()
427 .context("finding cargo executable")?
428 .ok_or_else(|| anyhow!("cargo executable not found; is Rust installed and in PATH?"))?;
429
430 let rustc_exe = self
431 .rustc_exe()
432 .context("finding rustc executable")?
433 .ok_or_else(|| anyhow!("rustc executable not found; is Rust installed and in PATH?"))?;
434
435 let rust_version =
436 rustc_version::VersionMeta::for_command(std::process::Command::new(&rustc_exe))
437 .context("resolving rustc version")?;
438
439 if rust_version.semver.lt(&MINIMUM_RUST_VERSION) {
440 return Err(anyhow!(
441 "PyOxidizer requires Rust {}; {} is version {}",
442 *MINIMUM_RUST_VERSION,
443 rustc_exe.display(),
444 rust_version.semver
445 ));
446 }
447
448 Ok(RustEnvironment {
449 cargo_exe,
450 rustc_exe,
451 rust_version,
452 })
453 }
454
455 pub fn resolve_apple_sdk(&self, sdk_info: &AppleSdkInfo) -> Result<ParsedSdk> {
457 let platform = &sdk_info.platform;
458 let minimum_version = &sdk_info.version;
459 let deployment_target = &sdk_info.deployment_target;
460
461 warn!(
462 "locating Apple SDK {}{}+ supporting {}{}",
463 platform, minimum_version, platform, deployment_target
464 );
465
466 let sdks = SdkSearch::default()
467 .progress_callback(|event| {
468 info!("{}", event);
469 })
470 .location(SdkSearchLocation::SystemXcodes)
472 .platform(platform.as_str().try_into()?)
473 .minimum_version(minimum_version)
474 .deployment_target(platform, deployment_target)
475 .sorting(SdkSorting::VersionDescending)
476 .search::<ParsedSdk>()?;
477
478 if sdks.is_empty() {
479 return Err(anyhow!(
480 "unable to find suitable Apple SDK supporting {}{} or newer",
481 platform,
482 minimum_version
483 ));
484 }
485
486 let sdk = sdks.into_iter().next().unwrap();
488
489 if sdk
490 .version()
491 .expect("ParsedSDK should always have version")
492 .clone()
493 < minimum_version.as_str().into()
494 {
495 warn!(
496 "WARNING: SDK does not meet minimum version requirement of {}; build errors or unexpected behavior may occur",
497 minimum_version
498 );
499 }
500
501 warn!(
502 "using {} targeting {}{}",
503 sdk.sdk_path(),
504 platform,
505 deployment_target
506 );
507
508 Ok(sdk)
509 }
510
511 pub fn temporary_directory(&self, prefix: &str) -> Result<tempfile::TempDir> {
513 let mut builder = tempfile::Builder::new();
514 builder.prefix(prefix);
515
516 if let Some(target_dir) = &self.cargo_target_directory {
517 let base = target_dir.join("tempdir");
518 std::fs::create_dir_all(&base)
519 .context("creating temporary directory base in cargo target dir")?;
520
521 builder.tempdir_in(&base)
522 } else {
523 builder.tempdir()
524 }
525 .context("creating temporary directory")
526 }
527}
528
529#[derive(Clone, Debug)]
531pub struct RustEnvironment {
532 pub cargo_exe: PathBuf,
534
535 pub rustc_exe: PathBuf,
537
538 pub rust_version: rustc_version::VersionMeta,
540}