pyoxidizerlib/py_packaging/
distutils.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5/*!
6Interacting with distutils.
7*/
8
9use {
10    anyhow::{Context, Result},
11    log::warn,
12    once_cell::sync::Lazy,
13    python_packaging::resource::{LibraryDependency, PythonExtensionModule},
14    serde::Deserialize,
15    simple_file_manifest::FileData,
16    std::{
17        collections::{BTreeMap, HashMap},
18        fs::{create_dir_all, read_dir, read_to_string},
19        path::{Path, PathBuf},
20    },
21};
22
23static MODIFIED_DISTUTILS_FILES: Lazy<BTreeMap<&'static str, &'static [u8]>> = Lazy::new(|| {
24    let mut res: BTreeMap<&'static str, &'static [u8]> = BTreeMap::new();
25
26    res.insert(
27        "command/build_ext.py",
28        include_bytes!("../distutils/command/build_ext.py"),
29    );
30    res.insert(
31        "_msvccompiler.py",
32        include_bytes!("../distutils/_msvccompiler.py"),
33    );
34    res.insert(
35        "unixccompiler.py",
36        include_bytes!("../distutils/unixccompiler.py"),
37    );
38
39    res
40});
41
42/// Prepare a hacked install of distutils to use with Python packaging.
43///
44/// The idea is we use the distutils in the distribution as a base then install
45/// our own hacks on top of it to make it perform the functionality that we want.
46/// This enables things using it (like setup.py scripts) to invoke our
47/// functionality, without requiring them to change anything.
48///
49/// An alternate considered implementation was to "prepend" code to the invoked
50/// setup.py or Python process so that the in-process distutils was monkeypatched.
51/// This approach felt less robust than modifying distutils itself because a
52/// modified distutils will survive multiple process invocations, unlike a
53/// monkeypatch. People do weird things in setup.py scripts and we want to
54/// support as many as possible.
55pub fn prepare_hacked_distutils(
56    orig_distutils_path: &Path,
57    dest_dir: &Path,
58    extra_python_paths: &[&Path],
59) -> Result<HashMap<String, String>> {
60    let extra_sys_path = dest_dir.join("packages");
61
62    warn!(
63        "installing modified distutils to {}",
64        extra_sys_path.display()
65    );
66
67    let dest_distutils_path = extra_sys_path.join("distutils");
68
69    for entry in walkdir::WalkDir::new(orig_distutils_path) {
70        let entry = entry?;
71
72        if entry.path().is_dir() {
73            continue;
74        }
75
76        let source_path = entry.path();
77        let rel_path = source_path
78            .strip_prefix(orig_distutils_path)
79            .with_context(|| format!("stripping prefix from {}", source_path.display()))?;
80        let dest_path = dest_distutils_path.join(rel_path);
81
82        let dest_dir = dest_path.parent().unwrap();
83        std::fs::create_dir_all(dest_dir)?;
84        std::fs::copy(source_path, &dest_path)?;
85    }
86
87    for (path, data) in MODIFIED_DISTUTILS_FILES.iter() {
88        let dest_path = dest_distutils_path.join(path);
89
90        warn!("modifying distutils/{} for oxidation", path);
91        std::fs::write(&dest_path, data)
92            .with_context(|| format!("writing {}", dest_path.display()))?;
93    }
94
95    let state_dir = dest_dir.join("pyoxidizer-build-state");
96    create_dir_all(&state_dir)?;
97
98    let mut python_paths = vec![extra_sys_path.display().to_string()];
99    python_paths.extend(extra_python_paths.iter().map(|p| p.display().to_string()));
100
101    let path_separator = if cfg!(windows) { ";" } else { ":" };
102
103    let python_path = python_paths.join(path_separator);
104
105    let mut res = HashMap::new();
106    res.insert("PYTHONPATH".to_string(), python_path);
107    res.insert(
108        "PYOXIDIZER_DISTUTILS_STATE_DIR".to_string(),
109        state_dir.display().to_string(),
110    );
111    res.insert("PYOXIDIZER".to_string(), "1".to_string());
112
113    Ok(res)
114}
115
116#[derive(Debug, Deserialize)]
117struct DistutilsExtensionState {
118    name: String,
119    objects: Vec<String>,
120    output_filename: String,
121    libraries: Vec<String>,
122    #[allow(dead_code)]
123    library_dirs: Vec<String>,
124    #[allow(dead_code)]
125    runtime_library_dirs: Vec<String>,
126}
127
128pub fn read_built_extensions(state_dir: &Path) -> Result<Vec<PythonExtensionModule>> {
129    let mut res = Vec::new();
130
131    let entries = read_dir(state_dir).context(format!(
132        "reading built extensions from {}",
133        state_dir.display()
134    ))?;
135
136    for entry in entries {
137        let entry = entry?;
138        let path = entry.path();
139        let file_name = path.file_name().unwrap().to_str().unwrap();
140
141        if !file_name.starts_with("extension.") || !file_name.ends_with(".json") {
142            continue;
143        }
144
145        let data = read_to_string(&path).context(format!("reading {}", path.display()))?;
146
147        let info: DistutilsExtensionState = serde_json::from_str(&data).context("parsing JSON")?;
148
149        let module_components: Vec<&str> = info.name.split('.').collect();
150        let final_name = module_components[module_components.len() - 1];
151        let init_fn = "PyInit_".to_string() + final_name;
152
153        let extension_path = PathBuf::from(&info.output_filename);
154
155        // Extension file suffix is the part after the first dot in the filename.
156        let extension_file_name = extension_path
157            .file_name()
158            .unwrap()
159            .to_string_lossy()
160            .to_string();
161
162        let extension_file_suffix = if let Some(idx) = extension_file_name.find('.') {
163            extension_file_name[idx..extension_file_name.len()].to_string()
164        } else {
165            extension_file_name
166        };
167
168        // Extension files may not always be written. So ignore errors on missing file.
169        let extension_data = if let Ok(data) = std::fs::read(&extension_path) {
170            Some(FileData::Memory(data))
171        } else {
172            None
173        };
174
175        let mut object_file_data = Vec::new();
176
177        for object_path in &info.objects {
178            let path = PathBuf::from(object_path);
179            let data = std::fs::read(&path).context(format!("reading {}", path.display()))?;
180
181            object_file_data.push(FileData::Memory(data));
182        }
183
184        let link_libraries = info
185            .libraries
186            .iter()
187            .map(|l| LibraryDependency {
188                name: l.clone(),
189                static_library: None,
190                static_filename: None,
191                dynamic_library: None,
192                dynamic_filename: None,
193                framework: false,
194                system: false,
195            })
196            .collect();
197
198        // TODO packaging rule functionality for requiring / denying shared library
199        // linking, annotating licenses of 3rd party libraries, disabling libraries
200        // wholesale, etc.
201
202        res.push(PythonExtensionModule {
203            name: info.name.clone(),
204            init_fn: Some(init_fn),
205            extension_file_suffix,
206            shared_library: extension_data,
207            object_file_data,
208            is_package: final_name == "__init__",
209            link_libraries,
210            is_stdlib: false,
211            builtin_default: false,
212            required: false,
213            variant: None,
214            license: None,
215        });
216    }
217
218    Ok(res)
219}