pyoxidizerlib/starlark/
eval.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::{
7        environment::default_target_triple,
8        py_packaging::distribution::DistributionCache,
9        starlark::env::{
10            populate_environment, register_starlark_dialect, PyOxidizerContext,
11            PyOxidizerEnvironmentContext,
12        },
13    },
14    anyhow::{anyhow, Result},
15    codemap::CodeMap,
16    codemap_diagnostic::{Diagnostic, Emitter},
17    log::error,
18    starlark::{
19        environment::{Environment, EnvironmentError, TypeValues},
20        eval::call_stack::CallStack,
21        syntax::dialect::Dialect,
22        values::{
23            error::{RuntimeError, ValueError},
24            Value, ValueResult,
25        },
26    },
27    starlark_dialect_build_targets::{
28        build_target, run_target, EnvironmentContext, ResolvedTarget,
29    },
30    std::{
31        collections::HashMap,
32        path::{Path, PathBuf},
33        sync::{Arc, Mutex},
34    },
35};
36
37/// Builder type to construct `EvaluationContext` instances.
38pub struct EvaluationContextBuilder {
39    env: crate::environment::Environment,
40    config_path: PathBuf,
41    build_target_triple: String,
42    release: bool,
43    verbose: bool,
44    resolve_targets: Option<Vec<String>>,
45    build_script_mode: bool,
46    build_opt_level: String,
47    distribution_cache: Option<Arc<DistributionCache>>,
48    extra_vars: HashMap<String, Option<String>>,
49}
50
51impl EvaluationContextBuilder {
52    pub fn new(
53        env: &crate::environment::Environment,
54        config_path: impl AsRef<Path>,
55        build_target_triple: impl ToString,
56    ) -> Self {
57        Self {
58            env: env.clone(),
59            config_path: config_path.as_ref().to_path_buf(),
60            build_target_triple: build_target_triple.to_string(),
61            release: false,
62            verbose: false,
63            resolve_targets: None,
64            build_script_mode: false,
65            build_opt_level: "0".to_string(),
66            distribution_cache: None,
67            extra_vars: HashMap::new(),
68        }
69    }
70
71    /// Transform self into an `EvaluationContext`.
72    pub fn into_context(self) -> Result<EvaluationContext> {
73        EvaluationContext::from_builder(self)
74    }
75
76    #[must_use]
77    pub fn config_path(mut self, value: impl AsRef<Path>) -> Self {
78        self.config_path = value.as_ref().to_path_buf();
79        self
80    }
81
82    #[must_use]
83    pub fn build_target_triple(mut self, value: impl ToString) -> Self {
84        self.build_target_triple = value.to_string();
85        self
86    }
87
88    #[must_use]
89    pub fn release(mut self, value: bool) -> Self {
90        self.release = value;
91        self
92    }
93
94    #[must_use]
95    pub fn verbose(mut self, value: bool) -> Self {
96        self.verbose = value;
97        self
98    }
99
100    #[must_use]
101    pub fn resolve_targets_optional(mut self, targets: Option<Vec<impl ToString>>) -> Self {
102        self.resolve_targets =
103            targets.map(|targets| targets.iter().map(|x| x.to_string()).collect());
104        self
105    }
106
107    #[must_use]
108    pub fn resolve_targets(mut self, targets: Vec<String>) -> Self {
109        self.resolve_targets = Some(targets);
110        self
111    }
112
113    #[must_use]
114    pub fn resolve_target_optional(mut self, target: Option<impl ToString>) -> Self {
115        self.resolve_targets = target.map(|target| vec![target.to_string()]);
116        self
117    }
118
119    #[must_use]
120    pub fn resolve_target(mut self, target: impl ToString) -> Self {
121        self.resolve_targets = Some(vec![target.to_string()]);
122        self
123    }
124
125    #[must_use]
126    pub fn build_script_mode(mut self, value: bool) -> Self {
127        self.build_script_mode = value;
128        self
129    }
130
131    #[must_use]
132    pub fn distribution_cache(mut self, cache: Arc<DistributionCache>) -> Self {
133        self.distribution_cache = Some(cache);
134        self
135    }
136
137    #[must_use]
138    pub fn extra_vars(mut self, extra_vars: HashMap<String, Option<String>>) -> Self {
139        self.extra_vars = extra_vars;
140        self
141    }
142}
143
144/// Interface to evaluate Starlark configuration files.
145///
146/// This type provides the primary interface for evaluating Starlark
147/// configuration files.
148///
149/// Instances should be constructed from `EvaluationContextBuilder` instances, as
150/// the number of parameters to construct an evaluation context is significant.
151pub struct EvaluationContext {
152    parent_env: Environment,
153    child_env: Environment,
154    type_values: TypeValues,
155}
156
157impl TryFrom<EvaluationContextBuilder> for EvaluationContext {
158    type Error = anyhow::Error;
159
160    fn try_from(value: EvaluationContextBuilder) -> Result<Self, Self::Error> {
161        Self::from_builder(value)
162    }
163}
164
165impl EvaluationContext {
166    pub fn from_builder(builder: EvaluationContextBuilder) -> Result<Self> {
167        let context = PyOxidizerEnvironmentContext::new(
168            &builder.env,
169            builder.verbose,
170            &builder.config_path,
171            default_target_triple(),
172            &builder.build_target_triple,
173            builder.release,
174            &builder.build_opt_level,
175            builder.distribution_cache,
176            builder.extra_vars,
177        )?;
178
179        let (mut parent_env, mut type_values) = starlark::stdlib::global_environment();
180
181        register_starlark_dialect(&mut parent_env, &mut type_values)
182            .map_err(|e| anyhow!("error creating Starlark environment: {:?}", e))?;
183
184        // All variables go in a child environment. Upon calling child(), the parent
185        // environment is frozen and no new changes are allowed.
186        let mut child_env = parent_env.child("pyoxidizer");
187
188        populate_environment(
189            &mut child_env,
190            &mut type_values,
191            context,
192            builder.resolve_targets,
193            builder.build_script_mode,
194        )
195        .map_err(|e| anyhow!("error populating Starlark environment: {:?}", e))?;
196
197        Ok(Self {
198            parent_env,
199            child_env,
200            type_values,
201        })
202    }
203
204    /// Obtain a named variable from the Starlark environment.
205    pub fn get_var(&self, name: &str) -> Result<Value, EnvironmentError> {
206        self.child_env.get(name)
207    }
208
209    /// Set a named variables in the Starlark environment.
210    pub fn set_var(&mut self, name: &str, value: Value) -> Result<(), EnvironmentError> {
211        self.child_env.set(name, value)
212    }
213
214    /// Evaluate a Starlark configuration file, returning a Diagnostic on error.
215    pub fn evaluate_file_diagnostic(&mut self, config_path: &Path) -> Result<(), Diagnostic> {
216        let map = Arc::new(Mutex::new(CodeMap::new()));
217        let file_loader_env = self.parent_env.clone();
218
219        starlark::eval::simple::eval_file(
220            &map,
221            &config_path.display().to_string(),
222            Dialect::Bzl,
223            &mut self.child_env,
224            &self.type_values,
225            file_loader_env,
226        )
227        .map_err(|e| {
228            let mut msg = Vec::new();
229            let raw_map = map.lock().unwrap();
230            {
231                let mut emitter = codemap_diagnostic::Emitter::vec(&mut msg, Some(&raw_map));
232                emitter.emit(&[e.clone()]);
233            }
234
235            error!("{}", String::from_utf8_lossy(&msg));
236
237            e
238        })?;
239
240        Ok(())
241    }
242
243    /// Evaluate a Starlark configuration file, returning an anyhow Result.
244    pub fn evaluate_file(&mut self, config_path: &Path) -> Result<()> {
245        self.evaluate_file_diagnostic(config_path)
246            .map_err(|d| anyhow!(d.message))
247    }
248
249    /// Evaluate code, returning a `Diagnostic` on error.
250    pub fn eval_diagnostic(
251        &mut self,
252        map: &Arc<Mutex<CodeMap>>,
253        path: &str,
254        code: &str,
255    ) -> Result<Value, Diagnostic> {
256        let file_loader_env = self.child_env.clone();
257
258        starlark::eval::simple::eval(
259            map,
260            path,
261            code,
262            Dialect::Bzl,
263            &mut self.child_env,
264            &self.type_values,
265            file_loader_env,
266        )
267    }
268
269    /// Evaluate code as if it is executing from a path.
270    pub fn eval_code_with_path(&mut self, path: &str, code: &str) -> Result<Value> {
271        let map = std::sync::Arc::new(std::sync::Mutex::new(CodeMap::new()));
272
273        self.eval_diagnostic(&map, path, code)
274            .map_err(|diagnostic| {
275                let cloned_map_lock = Arc::clone(&map);
276                let unlocked_map = cloned_map_lock.lock().unwrap();
277
278                let mut buffer = vec![];
279                Emitter::vec(&mut buffer, Some(&unlocked_map)).emit(&[diagnostic]);
280
281                anyhow!(
282                    "error running '{}': {}",
283                    code,
284                    String::from_utf8_lossy(&buffer)
285                )
286            })
287    }
288
289    /// Evaluate code with a placeholder value for the filename.
290    pub fn eval(&mut self, code: &str) -> Result<Value> {
291        self.eval_code_with_path("<no_file>", code)
292    }
293
294    /// Obtain the `Value` for the build targets context.
295    fn build_targets_context_value(&self) -> Result<Value> {
296        starlark_dialect_build_targets::get_context_value(&self.type_values)
297            .map_err(|_| anyhow!("could not obtain build targets context"))
298    }
299
300    /// Obtain the `Value` for the PyOxidizerContext.
301    pub fn pyoxidizer_context_value(&self) -> ValueResult {
302        self.type_values
303            .get_type_value(&Value::new(PyOxidizerContext::default()), "CONTEXT")
304            .ok_or_else(|| {
305                ValueError::from(RuntimeError {
306                    code: "PYOXIDIZER",
307                    message: "Unable to resolve context (this should never happen)".to_string(),
308                    label: "".to_string(),
309                })
310            })
311    }
312
313    pub fn build_path(&self) -> Result<PathBuf, ValueError> {
314        let pyoxidizer_context_value = self.pyoxidizer_context_value()?;
315        let pyoxidizer_context = pyoxidizer_context_value
316            .downcast_ref::<PyOxidizerEnvironmentContext>()
317            .ok_or(ValueError::IncorrectParameterType)?;
318
319        pyoxidizer_context.build_path(&self.type_values)
320    }
321
322    pub fn target_build_path(&self, target: &str) -> Result<PathBuf> {
323        let context_value = self.build_targets_context_value()?;
324        let context = context_value.downcast_ref::<EnvironmentContext>().unwrap();
325
326        Ok(context.target_build_path(target))
327    }
328
329    pub fn default_target(&self) -> Result<Option<String>> {
330        let raw_context = self.build_targets_context_value()?;
331        let context = raw_context
332            .downcast_ref::<EnvironmentContext>()
333            .ok_or_else(|| anyhow!("context has incorrect type"))?;
334
335        Ok(context.default_target().map(|x| x.to_string()))
336    }
337
338    pub fn target_names(&self) -> Result<Vec<String>> {
339        let raw_context = self.build_targets_context_value()?;
340        let context = raw_context
341            .downcast_ref::<EnvironmentContext>()
342            .ok_or_else(|| anyhow!("context has incorrect type"))?;
343
344        Ok(context
345            .targets()
346            .keys()
347            .map(|x| x.to_string())
348            .collect::<Vec<_>>())
349    }
350
351    /// Obtain targets that should be resolved.
352    pub fn targets_to_resolve(&self) -> Result<Vec<String>> {
353        let raw_context = self.build_targets_context_value()?;
354        let context = raw_context
355            .downcast_ref::<EnvironmentContext>()
356            .ok_or_else(|| anyhow!("context has incorrect type"))?;
357
358        Ok(context.targets_to_resolve())
359    }
360
361    pub fn build_resolved_target(&mut self, target: &str) -> Result<ResolvedTarget> {
362        let mut call_stack = CallStack::default();
363
364        build_target(
365            &mut self.child_env,
366            &self.type_values,
367            &mut call_stack,
368            target,
369        )
370    }
371
372    pub fn run_target(&mut self, target: Option<&str>) -> Result<()> {
373        let mut call_stack = CallStack::default();
374
375        run_target(
376            &mut self.child_env,
377            &self.type_values,
378            &mut call_stack,
379            target,
380        )
381    }
382}
383
384#[cfg(test)]
385mod tests {
386    use {super::*, crate::testutil::*, starlark::values::dict::Dictionary};
387
388    #[test]
389    fn test_load() -> Result<()> {
390        let env = get_env()?;
391        let temp_dir = env.temporary_directory("pyoxidizer-test")?;
392
393        let load_path = temp_dir.path().join("load.bzl");
394        std::fs::write(
395            &load_path,
396            "def make_dist():\n    return default_python_distribution()\n".as_bytes(),
397        )?;
398
399        let main_path = temp_dir.path().join("main.bzl");
400        std::fs::write(
401            &main_path,
402            format!(
403                "load('{}', 'make_dist')\nmake_dist()\n",
404                load_path.display().to_string().escape_default()
405            )
406            .as_bytes(),
407        )?;
408
409        let mut context = EvaluationContextBuilder::new(
410            &env,
411            main_path.clone(),
412            default_target_triple().to_string(),
413        )
414        .verbose(true)
415        .into_context()?;
416        context.evaluate_file(&main_path)?;
417
418        temp_dir.close()?;
419
420        Ok(())
421    }
422
423    #[test]
424    fn test_register_target() -> Result<()> {
425        let env = get_env()?;
426        let temp_dir = env.temporary_directory("pyoxidizer-test")?;
427
428        let config_path = temp_dir.path().join("pyoxidizer.bzl");
429        std::fs::write(&config_path, "def make_dist():\n    return default_python_distribution()\nregister_target('dist', make_dist)\n".as_bytes())?;
430
431        let mut context: EvaluationContext = EvaluationContextBuilder::new(
432            &env,
433            config_path.clone(),
434            default_target_triple().to_string(),
435        )
436        .verbose(true)
437        .into_context()?;
438        context.evaluate_file(&config_path)?;
439
440        temp_dir.close()?;
441
442        Ok(())
443    }
444
445    #[test]
446    fn extra_vars() -> Result<()> {
447        let env = get_env()?;
448        let temp_dir = env.temporary_directory("pyoxidizer-test")?;
449
450        let config_path = temp_dir.path().join("pyoxidizer.bzl");
451        std::fs::write(&config_path, "my_var_copy = my_var\nempty_copy = empty\n")?;
452
453        let mut extra_vars = HashMap::new();
454        extra_vars.insert("my_var".to_string(), Some("my_value".to_string()));
455        extra_vars.insert("empty".to_string(), None);
456
457        let context =
458            EvaluationContextBuilder::new(&env, config_path, default_target_triple().to_string())
459                .extra_vars(extra_vars)
460                .into_context()?;
461
462        let vars_value = context.get_var("VARS").unwrap();
463        assert_eq!(vars_value.get_type(), "dict");
464        let vars = vars_value.downcast_ref::<Dictionary>().unwrap();
465
466        let v = vars.get(&Value::from("my_var")).unwrap().unwrap();
467        assert_eq!(v.to_string(), "my_value");
468
469        let v = vars.get(&Value::from("empty")).unwrap().unwrap();
470        assert_eq!(v.get_type(), "NoneType");
471
472        temp_dir.close()?;
473
474        Ok(())
475    }
476}