rattler/install/
entry_point.rs

1use crate::install::PythonInfo;
2use digest::Output;
3use rattler_conda_types::{
4    package::EntryPoint,
5    prefix_record::{PathType, PathsEntry},
6    Platform,
7};
8use rattler_digest::HashingWriter;
9use rattler_digest::Sha256;
10use std::{fs::File, io, io::Write, path::Path};
11
12use super::Prefix;
13
14/// Get the bytes of the windows launcher executable.
15pub fn get_windows_launcher(platform: &Platform) -> &'static [u8] {
16    match platform {
17        Platform::Win32 => unimplemented!("32 bit windows is not supported for entry points"),
18        Platform::Win64 => include_bytes!("../../resources/launcher64.exe"),
19        Platform::WinArm64 => unimplemented!("arm64 windows is not supported for entry points"),
20        _ => panic!("unsupported platform"),
21    }
22}
23
24/// Creates an "entry point" on disk for a Python entrypoint. Entrypoints are executable files that
25/// directly call a certain Python function.
26///
27/// On unix this is pretty trivial through the use of an executable shell script that invokes the
28/// python compiler which in turn invokes the correct Python function. On windows however, this
29/// mechanism doesn't exists. Instead a special executable is copied that starts a Python interpreter
30/// which executes a file that is named the same as the executable but with the `.py` file
31/// extension. So if there is an entry point file called `foo.py` an executable is created called
32/// `foo.exe` that will automatically invoke `foo.py`.
33///
34/// The special executable is embedded in the library. The source code for the launcher can be found
35/// here: <https://github.com/conda/conda-build/tree/master/conda_build/launcher_sources>.
36///
37/// See [`create_unix_python_entry_point`] for the unix variant of this function.
38pub fn create_windows_python_entry_point(
39    target_dir: &Prefix,
40    target_prefix: &str,
41    entry_point: &EntryPoint,
42    python_info: &PythonInfo,
43    target_platform: &Platform,
44) -> Result<[PathsEntry; 2], std::io::Error> {
45    // Construct the path to where we will be creating the python entry point script.
46    let relative_path_script_py = python_info
47        .bin_dir
48        .join(format!("{}-script.py", &entry_point.command));
49
50    // Write the contents of the launcher script to disk
51    let script_path = target_dir.path().join(&relative_path_script_py);
52    std::fs::create_dir_all(
53        script_path
54            .parent()
55            .expect("since we joined with target_dir there must be a parent"),
56    )?;
57    let script_contents =
58        python_entry_point_template(target_prefix, true, entry_point, python_info);
59    let (hash, size) = write_and_hash(&script_path, script_contents)?;
60
61    // Construct a path to where we will create the python launcher executable.
62    let relative_path_script_exe = python_info
63        .bin_dir
64        .join(format!("{}.exe", &entry_point.command));
65
66    // Include the bytes of the launcher directly in the binary so we can write it to disk.
67    let launcher_bytes = get_windows_launcher(target_platform);
68    std::fs::write(
69        target_dir.path().join(&relative_path_script_exe),
70        launcher_bytes,
71    )?;
72
73    let fixed_launcher_digest = rattler_digest::parse_digest_from_hex::<rattler_digest::Sha256>(
74        "28b001bb9a72ae7a24242bfab248d767a1ac5dec981c672a3944f7a072375e9a",
75    )
76    .unwrap();
77
78    Ok([
79        PathsEntry {
80            relative_path: relative_path_script_py,
81            // todo: clobbering of entry points not handled yet
82            original_path: None,
83            path_type: PathType::WindowsPythonEntryPointScript,
84            no_link: false,
85            sha256: Some(hash),
86            sha256_in_prefix: None,
87            size_in_bytes: Some(size as _),
88            prefix_placeholder: None,
89            file_mode: None,
90        },
91        PathsEntry {
92            relative_path: relative_path_script_exe,
93            original_path: None,
94            path_type: PathType::WindowsPythonEntryPointExe,
95            no_link: false,
96            sha256: Some(fixed_launcher_digest),
97            sha256_in_prefix: None,
98            size_in_bytes: Some(launcher_bytes.len() as u64),
99            prefix_placeholder: None,
100            file_mode: None,
101        },
102    ])
103}
104
105/// Creates an "entry point" on disk for a Python entrypoint. Entrypoints are executable files that
106/// directly call a certain Python function.
107///
108/// On unix this is pretty trivial through the use of an executable shell script that invokes the
109/// python compiler which in turn invokes the correct Python function.
110///
111/// On windows things are a bit more complicated. See [`create_windows_python_entry_point`].
112pub fn create_unix_python_entry_point(
113    target_dir: &Prefix,
114    target_prefix: &str,
115    entry_point: &EntryPoint,
116    python_info: &PythonInfo,
117) -> Result<PathsEntry, std::io::Error> {
118    // Construct the path to where we will be creating the python entry point script.
119    let relative_path = python_info.bin_dir.join(&entry_point.command);
120
121    // Write the contents of the launcher script to disk
122    let script_path = target_dir.path().join(&relative_path);
123    std::fs::create_dir_all(
124        script_path
125            .parent()
126            .expect("since we joined with target_dir there must be a parent"),
127    )?;
128    let script_contents =
129        python_entry_point_template(target_prefix, false, entry_point, python_info);
130    let (hash, size) = write_and_hash(&script_path, script_contents)?;
131
132    // Make the script executable. This is only supported on Unix based filesystems.
133    #[cfg(unix)]
134    std::fs::set_permissions(
135        script_path,
136        std::os::unix::fs::PermissionsExt::from_mode(0o775),
137    )?;
138
139    Ok(PathsEntry {
140        relative_path,
141        // todo: clobbering of entry points not handled yet
142        original_path: None,
143        path_type: PathType::UnixPythonEntryPoint,
144        no_link: false,
145        sha256: Some(hash),
146        sha256_in_prefix: None,
147        size_in_bytes: Some(size as _),
148        prefix_placeholder: None,
149        file_mode: None,
150    })
151}
152
153/// Returns Python code that, when placed in an executable file, invokes the specified
154/// [`EntryPoint`].
155pub fn python_entry_point_template(
156    target_prefix: &str,
157    for_windows: bool,
158    entry_point: &EntryPoint,
159    python_info: &PythonInfo,
160) -> String {
161    // Construct a shebang for the python interpreter
162    let shebang = if for_windows {
163        // On windows we don't need a shebang. Adding a shebang actually breaks the launcher
164        // for prefixes with spaces.
165        String::new()
166    } else {
167        python_info.shebang(target_prefix)
168    };
169
170    // The name of the module to import to be able to call the function
171    let (import_name, _) = entry_point
172        .function
173        .split_once('.')
174        .unwrap_or((&entry_point.function, ""));
175
176    let module = &entry_point.module;
177    let func = &entry_point.function;
178    format!(
179        "{shebang}\n\
180        # -*- coding: utf-8 -*-\n\
181        import re\n\
182        import sys\n\n\
183        from {module} import {import_name}\n\n\
184        if __name__ == '__main__':\n\
185        \tsys.argv[0] = re.sub(r'(-script\\.pyw?|\\.exe)?$', '', sys.argv[0])\n\
186        \tsys.exit({func}())\n\
187        "
188    )
189}
190
191/// Writes the given bytes to a file and records the hash, as well as the size of the file.
192fn write_and_hash(path: &Path, contents: impl AsRef<[u8]>) -> io::Result<(Output<Sha256>, usize)> {
193    let bytes = contents.as_ref();
194    let mut writer = HashingWriter::<_, Sha256>::new(File::create(path)?);
195    writer.write_all(bytes)?;
196    let (_, hash) = writer.finalize();
197    Ok((hash, bytes.len()))
198}
199
200#[cfg(test)]
201mod test {
202    use crate::install::PythonInfo;
203    use rattler_conda_types::package::EntryPoint;
204    use rattler_conda_types::{Platform, Version};
205    use std::str::FromStr;
206
207    #[test]
208    fn test_entry_point_script() {
209        let script = super::python_entry_point_template(
210            "/prefix",
211            false,
212            &EntryPoint::from_str("jupyter-lab = jupyterlab.labapp:main").unwrap(),
213            &PythonInfo::from_version(
214                &Version::from_str("3.11.0").unwrap(),
215                None,
216                Platform::Linux64,
217            )
218            .unwrap(),
219        );
220        insta::assert_snapshot!(script);
221
222        let script = super::python_entry_point_template(
223            "/prefix",
224            true,
225            &EntryPoint::from_str("jupyter-lab = jupyterlab.labapp:main").unwrap(),
226            &PythonInfo::from_version(
227                &Version::from_str("3.11.0").unwrap(),
228                None,
229                Platform::Linux64,
230            )
231            .unwrap(),
232        );
233        insta::assert_snapshot!("windows", script);
234    }
235}