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}