use {
super::{
binary::{LibpythonLinkMode, PythonBinaryBuilder},
config::PyembedPythonInterpreterConfig,
standalone_distribution::StandaloneDistribution,
},
crate::python_distributions::PYTHON_DISTRIBUTIONS,
anyhow::{anyhow, Context, Result},
fs2::FileExt,
python_packaging::{
bytecode::PythonBytecodeCompiler, module_util::PythonModuleSuffixes,
policy::PythonPackagingPolicy, resource::PythonResource,
},
sha2::{Digest, Sha256},
slog::warn,
std::{
collections::HashMap,
convert::TryFrom,
fs,
fs::{create_dir_all, File},
io::Read,
ops::DerefMut,
path::{Path, PathBuf},
sync::{Arc, Mutex},
},
tugger_common::http::get_http_client,
tugger_file_manifest::FileData,
url::Url,
uuid::Uuid,
};
#[derive(Clone, Debug, PartialEq)]
pub enum BinaryLibpythonLinkMode {
Default,
Static,
Dynamic,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum PythonDistributionLocation {
Local { local_path: String, sha256: String },
Url { url: String, sha256: String },
}
#[derive(Clone, Debug, PartialEq)]
pub struct PythonDistributionRecord {
pub python_major_minor_version: String,
pub location: PythonDistributionLocation,
pub target_triple: String,
pub supports_prebuilt_extension_modules: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct AppleSdkInfo {
pub canonical_name: String,
pub platform: String,
pub version: String,
pub deployment_target: String,
}
pub trait PythonDistribution {
fn clone_trait(&self) -> Arc<dyn PythonDistribution>;
fn target_triple(&self) -> &str;
fn compatible_host_triples(&self) -> Vec<String>;
fn python_exe_path(&self) -> &Path;
fn python_version(&self) -> &str;
fn python_major_minor_version(&self) -> String;
fn python_implementation(&self) -> &str;
fn python_implementation_short(&self) -> &str;
fn python_tag(&self) -> &str;
fn python_abi_tag(&self) -> Option<&str>;
fn python_platform_tag(&self) -> &str;
fn python_platform_compatibility_tag(&self) -> &str;
fn cache_tag(&self) -> &str;
fn python_module_suffixes(&self) -> Result<PythonModuleSuffixes>;
fn stdlib_test_packages(&self) -> Vec<String>;
fn apple_sdk_info(&self) -> Option<&AppleSdkInfo>;
fn create_bytecode_compiler(&self) -> Result<Box<dyn PythonBytecodeCompiler>>;
fn create_packaging_policy(&self) -> Result<PythonPackagingPolicy>;
fn create_python_interpreter_config(&self) -> Result<PyembedPythonInterpreterConfig>;
#[allow(clippy::too_many_arguments)]
fn as_python_executable_builder(
&self,
logger: &slog::Logger,
host_triple: &str,
target_triple: &str,
name: &str,
libpython_link_mode: BinaryLibpythonLinkMode,
policy: &PythonPackagingPolicy,
config: &PyembedPythonInterpreterConfig,
host_distribution: Option<Arc<dyn PythonDistribution>>,
) -> Result<Box<dyn PythonBinaryBuilder>>;
fn python_resources<'a>(&self) -> Vec<PythonResource<'a>>;
fn ensure_pip(&self, logger: &slog::Logger) -> Result<PathBuf>;
fn resolve_distutils(
&self,
logger: &slog::Logger,
libpython_link_mode: LibpythonLinkMode,
dest_dir: &Path,
extra_python_paths: &[&Path],
) -> Result<HashMap<String, String>>;
fn supports_in_memory_shared_library_loading(&self) -> bool;
fn is_stdlib_test_package(&self, name: &str) -> bool {
for package in self.stdlib_test_packages() {
let prefix = format!("{}.", package);
if name == package || name.starts_with(&prefix) {
return true;
}
}
false
}
fn tcl_files(&self) -> Result<Vec<(PathBuf, FileData)>>;
fn tcl_library_path_directory(&self) -> Option<String>;
}
pub struct DistributionExtractLock {
file: std::fs::File,
}
impl DistributionExtractLock {
pub fn new(extract_dir: &Path) -> Result<Self> {
let lock_path = extract_dir
.parent()
.unwrap()
.join("distribution-extract-lock");
let file = File::create(&lock_path)
.context(format!("could not create {}", lock_path.display()))?;
file.lock_exclusive()
.context(format!("failed to obtain lock for {}", lock_path.display()))?;
Ok(DistributionExtractLock { file })
}
}
impl Drop for DistributionExtractLock {
fn drop(&mut self) {
self.file.unlock().unwrap();
}
}
fn sha256_path(path: &Path) -> Vec<u8> {
let mut hasher = Sha256::new();
let fh = File::open(&path).unwrap();
let mut reader = std::io::BufReader::new(fh);
let mut buffer = [0; 32768];
loop {
let count = reader.read(&mut buffer).expect("error reading");
if count == 0 {
break;
}
hasher.update(&buffer[..count]);
}
hasher.finalize().to_vec()
}
pub fn download_distribution(url: &str, sha256: &str, cache_dir: &Path) -> Result<PathBuf> {
let expected_hash = hex::decode(sha256)?;
let u = Url::parse(url)?;
let basename = u
.path_segments()
.expect("cannot be base path")
.last()
.unwrap()
.to_string();
let cache_path = cache_dir.join(basename);
if cache_path.exists() {
let file_hash = sha256_path(&cache_path);
if file_hash == expected_hash {
return Ok(cache_path);
}
}
let mut data: Vec<u8> = Vec::new();
println!("downloading {}", u);
let client = get_http_client()?;
let mut response = client.get(u.as_str()).send()?;
response.read_to_end(&mut data)?;
let mut hasher = Sha256::new();
hasher.update(&data);
let url_hash = hasher.finalize().to_vec();
if url_hash != expected_hash {
return Err(anyhow!("sha256 of Python distribution does not validate"));
}
let mut temp_cache_path = cache_path.clone();
temp_cache_path.set_file_name(format!("{}.tmp", Uuid::new_v4()));
fs::write(&temp_cache_path, data).context("unable to write distribution file")?;
fs::rename(&temp_cache_path, &cache_path)
.or_else(|e| -> Result<()> {
fs::remove_file(&temp_cache_path)
.context("unable to remove temporary distribution file")?;
if cache_path.exists() {
download_distribution(url, sha256, cache_dir)?;
return Ok(());
}
Err(e.into())
})
.context("unable to rename downloaded distribution file")?;
Ok(cache_path)
}
pub fn copy_local_distribution(path: &Path, sha256: &str, cache_dir: &Path) -> Result<PathBuf> {
let expected_hash = hex::decode(sha256)?;
let basename = path.file_name().unwrap().to_str().unwrap().to_string();
let cache_path = cache_dir.join(basename);
if cache_path.exists() {
let file_hash = sha256_path(&cache_path);
if file_hash == expected_hash {
println!(
"existing {} passes SHA-256 integrity check",
cache_path.display()
);
return Ok(cache_path);
}
}
let source_hash = sha256_path(&path);
if source_hash != expected_hash {
return Err(anyhow!("sha256 of Python distribution does not validate"));
}
println!("copying {}", path.display());
std::fs::copy(path, &cache_path)?;
Ok(cache_path)
}
pub fn resolve_python_distribution_archive(
dist: &PythonDistributionLocation,
cache_dir: &Path,
) -> Result<PathBuf> {
if !cache_dir.exists() {
create_dir_all(cache_dir).unwrap();
}
match dist {
PythonDistributionLocation::Local { local_path, sha256 } => {
let p = PathBuf::from(local_path);
copy_local_distribution(&p, sha256, cache_dir)
}
PythonDistributionLocation::Url { url, sha256 } => {
download_distribution(url, sha256, cache_dir)
}
}
}
pub fn resolve_python_distribution_from_location(
logger: &slog::Logger,
location: &PythonDistributionLocation,
distributions_dir: &Path,
) -> Result<(PathBuf, PathBuf)> {
warn!(logger, "resolving Python distribution {:?}", location);
let path = resolve_python_distribution_archive(location, distributions_dir)?;
warn!(
logger,
"Python distribution available at {}",
path.display()
);
let distribution_hash = match location {
PythonDistributionLocation::Local { sha256, .. } => sha256,
PythonDistributionLocation::Url { sha256, .. } => sha256,
};
let distribution_path = distributions_dir.join(format!("python.{}", &distribution_hash[0..12]));
Ok((path, distribution_path))
}
#[derive(Debug, PartialEq)]
pub enum DistributionFlavor {
Standalone,
StandaloneStatic,
StandaloneDynamic,
}
impl Default for DistributionFlavor {
fn default() -> Self {
DistributionFlavor::Standalone
}
}
impl TryFrom<&str> for DistributionFlavor {
type Error = String;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"standalone" => Ok(Self::Standalone),
"standalone_static" | "standalone-static" => Ok(Self::StandaloneStatic),
"standalone_dynamic" | "standalone-dynamic" => Ok(Self::StandaloneDynamic),
_ => Err(format!("distribution flavor {} not recognized", value)),
}
}
}
type DistributionCacheKey = (PathBuf, PythonDistributionLocation);
type DistributionCacheValue = Arc<Mutex<Option<Arc<StandaloneDistribution>>>>;
#[derive(Debug)]
pub struct DistributionCache {
cache: Mutex<HashMap<DistributionCacheKey, DistributionCacheValue>>,
default_dest_dir: Option<PathBuf>,
}
impl DistributionCache {
pub fn new(default_dest_dir: Option<&Path>) -> Self {
Self {
cache: Mutex::new(HashMap::new()),
default_dest_dir: default_dest_dir.clone().map(|x| x.to_path_buf()),
}
}
pub fn resolve_distribution(
&self,
logger: &slog::Logger,
location: &PythonDistributionLocation,
dest_dir: Option<&Path>,
) -> Result<Arc<StandaloneDistribution>> {
let dest_dir = if let Some(p) = dest_dir {
p
} else if let Some(p) = &self.default_dest_dir {
p
} else {
return Err(anyhow!("no destination directory available"));
};
let key = (dest_dir.to_path_buf(), location.clone());
let entry = {
let mut lock = self
.cache
.lock()
.map_err(|e| anyhow!("cannot obtain distribution cache lock: {}", e))?;
if let Some(value) = lock.get(&key) {
value.clone()
} else {
let value = Arc::new(Mutex::new(None));
lock.insert(key.clone(), value.clone());
value
}
};
let mut lock = entry
.lock()
.map_err(|e| anyhow!("cannot obtain distribution lock: {}", e))?;
let value = lock.deref_mut();
if let Some(dist) = value {
Ok(dist.clone())
} else {
let dist = Arc::new(StandaloneDistribution::from_location(
logger, location, &dest_dir,
)?);
lock.replace(dist.clone());
Ok(dist)
}
}
}
#[allow(unused)]
pub fn resolve_distribution(
logger: &slog::Logger,
location: &PythonDistributionLocation,
dest_dir: &Path,
) -> Result<Box<dyn PythonDistribution>> {
Ok(Box::new(StandaloneDistribution::from_location(
logger, &location, dest_dir,
)?) as Box<dyn PythonDistribution>)
}
pub fn default_distribution_location(
flavor: &DistributionFlavor,
target: &str,
python_major_minor_version: Option<&str>,
) -> Result<PythonDistributionLocation> {
let dist = PYTHON_DISTRIBUTIONS
.find_distribution(target, flavor, python_major_minor_version)
.ok_or_else(|| anyhow!("could not find default Python distribution for {}", target))?;
Ok(dist.location)
}
#[cfg(test)]
mod tests {
use {super::*, crate::testutil::*};
#[test]
fn test_all_standalone_distributions() -> Result<()> {
assert!(!get_all_standalone_distributions()?.is_empty());
Ok(())
}
}