pyoxidizerlib/starlark/
env.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
5use {
6    crate::py_packaging::distribution::DistributionCache,
7    anyhow::{Context, Result},
8    starlark::{
9        environment::{Environment, EnvironmentError, TypeValues},
10        values::{
11            error::{RuntimeError, ValueError},
12            none::NoneType,
13            {Mutable, TypedValue, Value, ValueResult},
14        },
15    },
16    starlark_dialect_build_targets::{get_context_value, EnvironmentContext},
17    std::{
18        collections::HashMap,
19        path::{Path, PathBuf},
20        sync::Arc,
21    },
22    tugger::starlark::TuggerContext,
23};
24
25/// Holds state for evaluating a Starlark config file.
26#[derive(Debug)]
27pub struct PyOxidizerEnvironmentContext {
28    /// PyOxidizer's run-time environment.
29    env: crate::environment::Environment,
30
31    /// Whether executing in verbose mode.
32    pub verbose: bool,
33
34    /// Directory the environment should be evaluated from.
35    ///
36    /// Typically used to resolve filenames.
37    pub cwd: PathBuf,
38
39    /// Path to the configuration file.
40    pub config_path: PathBuf,
41
42    /// Host triple we are building from.
43    pub build_host_triple: String,
44
45    /// Target triple we are building for.
46    pub build_target_triple: String,
47
48    /// Whether we are building a debug or release binary.
49    pub build_release: bool,
50
51    /// Optimization level when building binaries.
52    pub build_opt_level: String,
53
54    /// Cache of ready-to-clone Python distribution objects.
55    ///
56    /// This exists because constructing a new instance can take a
57    /// few seconds in debug builds. And this adds up, especially in tests!
58    pub distribution_cache: Arc<DistributionCache>,
59
60    /// Extra variables to inject into Starlark environment.
61    extra_vars: HashMap<String, Option<String>>,
62}
63
64impl PyOxidizerEnvironmentContext {
65    #[allow(clippy::too_many_arguments)]
66    pub fn new(
67        env: &crate::environment::Environment,
68        verbose: bool,
69        config_path: &Path,
70        build_host_triple: &str,
71        build_target_triple: &str,
72        build_release: bool,
73        build_opt_level: &str,
74        distribution_cache: Option<Arc<DistributionCache>>,
75        extra_vars: HashMap<String, Option<String>>,
76    ) -> Result<PyOxidizerEnvironmentContext> {
77        let parent = config_path
78            .parent()
79            .with_context(|| "resolving parent directory of config".to_string())?;
80
81        let parent = if parent.is_relative() {
82            std::env::current_dir()?.join(parent)
83        } else {
84            parent.to_path_buf()
85        };
86
87        let distribution_cache = distribution_cache.unwrap_or_else(|| {
88            Arc::new(DistributionCache::new(Some(
89                &env.python_distributions_dir(),
90            )))
91        });
92
93        Ok(PyOxidizerEnvironmentContext {
94            env: env.clone(),
95            verbose,
96            cwd: parent,
97            config_path: config_path.to_path_buf(),
98            build_host_triple: build_host_triple.to_string(),
99            build_target_triple: build_target_triple.to_string(),
100            build_release,
101            build_opt_level: build_opt_level.to_string(),
102            distribution_cache,
103            extra_vars,
104        })
105    }
106
107    pub fn env(&self) -> &crate::environment::Environment {
108        &self.env
109    }
110
111    pub fn build_path(&self, type_values: &TypeValues) -> Result<PathBuf, ValueError> {
112        let build_targets_context_value = get_context_value(type_values)?;
113        let context = build_targets_context_value
114            .downcast_ref::<EnvironmentContext>()
115            .ok_or(ValueError::IncorrectParameterType)?;
116
117        Ok(context.build_path().to_path_buf())
118    }
119
120    pub fn python_distributions_path(&self) -> Result<PathBuf, ValueError> {
121        Ok(self.env.python_distributions_dir())
122    }
123
124    pub fn get_output_path(
125        &self,
126        type_values: &TypeValues,
127        target: &str,
128    ) -> Result<PathBuf, ValueError> {
129        let build_targets_context_value = get_context_value(type_values)?;
130        let context = build_targets_context_value
131            .downcast_ref::<EnvironmentContext>()
132            .ok_or(ValueError::IncorrectParameterType)?;
133
134        Ok(context.target_build_path(target))
135    }
136}
137
138impl TypedValue for PyOxidizerEnvironmentContext {
139    type Holder = Mutable<PyOxidizerEnvironmentContext>;
140    const TYPE: &'static str = "EnvironmentContext";
141
142    fn values_for_descendant_check_and_freeze(&self) -> Box<dyn Iterator<Item = Value>> {
143        Box::new(std::iter::empty())
144    }
145}
146
147/// Starlark type holding context for PyOxidizer.
148#[derive(Default)]
149pub struct PyOxidizerContext {}
150
151impl TypedValue for PyOxidizerContext {
152    type Holder = Mutable<PyOxidizerContext>;
153    const TYPE: &'static str = "PyOxidizer";
154
155    fn values_for_descendant_check_and_freeze(&self) -> Box<dyn Iterator<Item = Value>> {
156        Box::new(std::iter::empty())
157    }
158}
159
160/// Obtain the PyOxidizerContext for the Starlark execution environment.
161pub fn get_context(type_values: &TypeValues) -> ValueResult {
162    type_values
163        .get_type_value(&Value::new(PyOxidizerContext::default()), "CONTEXT")
164        .ok_or_else(|| {
165            ValueError::from(RuntimeError {
166                code: "PYOXIDIZER",
167                message: "Unable to resolve context (this should never happen)".to_string(),
168                label: "".to_string(),
169            })
170        })
171}
172
173/// Obtain a Starlark environment for evaluating PyOxidizer configurations.
174pub fn register_starlark_dialect(
175    env: &mut Environment,
176    type_values: &mut TypeValues,
177) -> Result<(), EnvironmentError> {
178    starlark_dialect_build_targets::register_starlark_dialect(env, type_values)?;
179    tugger::starlark::register_starlark_dialect(env, type_values)?;
180    super::file_resource::file_resource_env(env, type_values);
181    super::python_distribution::python_distribution_module(env, type_values);
182    super::python_embedded_resources::python_embedded_resources_module(env, type_values);
183    super::python_executable::python_executable_env(env, type_values);
184    super::python_packaging_policy::python_packaging_policy_module(env, type_values);
185
186    Ok(())
187}
188
189pub fn populate_environment(
190    env: &mut Environment,
191    type_values: &mut TypeValues,
192    context: PyOxidizerEnvironmentContext,
193    resolve_targets: Option<Vec<String>>,
194    build_script_mode: bool,
195) -> Result<(), EnvironmentError> {
196    let mut build_targets_context = EnvironmentContext::new(context.cwd.clone());
197
198    if let Some(targets) = resolve_targets {
199        build_targets_context.set_resolve_targets(targets);
200    }
201
202    build_targets_context.build_script_mode = build_script_mode;
203
204    build_targets_context.set_target_build_path_prefix(Some(
205        PathBuf::from(&context.build_target_triple).join(if context.build_release {
206            "release"
207        } else {
208            "debug"
209        }),
210    ));
211
212    let tugger_context = TuggerContext::new();
213
214    starlark_dialect_build_targets::populate_environment(env, type_values, build_targets_context)?;
215    tugger::starlark::populate_environment(env, type_values, tugger_context)?;
216
217    let mut vars = starlark::values::dict::Dictionary::default();
218
219    for (k, v) in context.extra_vars.iter() {
220        vars.insert(
221            Value::from(k.as_str()),
222            match v {
223                Some(v) => Value::from(v.as_str()),
224                None => Value::from(NoneType::None),
225            },
226        )
227        .expect("error inserting variable; this should not happen");
228    }
229
230    env.set("VARS", Value::try_from(vars.get_content().clone()).unwrap())?;
231    env.set("CWD", Value::from(context.cwd.display().to_string()))?;
232    env.set(
233        "CONFIG_PATH",
234        Value::from(context.config_path.display().to_string()),
235    )?;
236    env.set(
237        "BUILD_TARGET_TRIPLE",
238        Value::from(context.build_target_triple.clone()),
239    )?;
240
241    env.set("CONTEXT", Value::new(context))?;
242
243    // We alias various globals as PyOxidizer.* attributes so they are
244    // available via the type object API. This is a bit hacky. But it allows
245    // Rust code with only access to the TypeValues dictionary to retrieve
246    // these globals.
247    for f in &["CONTEXT", "CWD", "CONFIG_PATH", "BUILD_TARGET_TRIPLE"] {
248        type_values.add_type_value(PyOxidizerContext::TYPE, f, env.get(f)?);
249    }
250
251    Ok(())
252}
253
254#[cfg(test)]
255pub mod tests {
256    use crate::{environment::default_target_triple, starlark::testutil::*};
257
258    #[test]
259    fn test_cwd() {
260        let cwd = starlark_ok("CWD");
261        let pwd = std::env::current_dir().unwrap();
262        assert_eq!(cwd.to_str(), pwd.display().to_string());
263    }
264
265    #[test]
266    fn test_build_target() {
267        let target = starlark_ok("BUILD_TARGET_TRIPLE");
268        assert_eq!(target.to_str(), default_target_triple());
269    }
270
271    #[test]
272    fn test_print() {
273        starlark_ok("print('hello, world')");
274    }
275}