Skip to content

Commit 2a8937d

Browse files
committed
Switch to using // separator for module specifiers: @modules_name//some/subpath/script.axl
1 parent 2cdf94e commit 2a8937d

File tree

6 files changed

+169
-103
lines changed

6 files changed

+169
-103
lines changed

.aspect/elsewhere/user-task.axl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
load("@local//answer.axl", "ANSWER")
2+
13
def _impl(ctx):
24
print("I am a task that is not auto-discovered but manually registered in MODULE.aspect instead")
5+
print("The answer is", ANSWER)
36
return 0
47

58
user_task = task(

.aspect/modules/local/answer.axl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ANSWER = 42

crates/axl-runtime/src/builtins/aspect/common.axl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,3 @@ def tui(ctx: TaskContext, build: bazel.build.Build) -> int:
6767

6868
else:
6969
return _simple_tui(ctx, build)
70-

crates/axl-runtime/src/eval/load.rs

Lines changed: 86 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use starlark::eval::{Evaluator, FileLoader};
99
use starlark::syntax::AstModule;
1010

1111
use crate::eval::{AxlScriptEvaluator, LOAD_STACK};
12-
use crate::helpers::sanitize_load_path_lexically;
12+
use crate::helpers::{sanitize_load_path_lexically, LoadPath};
1313

1414
thread_local! {
1515
static LOADED_MODULES: RefCell<HashMap<String, FrozenModule>> = RefCell::new(HashMap::new());
@@ -24,6 +24,9 @@ pub struct AxlLoader<'a> {
2424
// The script dir which relative loads are relative to
2525
pub(super) script_dir: PathBuf,
2626

27+
// The current module name that the load statement is in
28+
pub(super) module_name: String,
29+
2730
// The module root directory that relative loads cannot escape
2831
pub(super) module_root: PathBuf,
2932

@@ -38,29 +41,57 @@ impl<'a> AxlLoader<'a> {
3841
module_subpath: &Path,
3942
) -> starlark::Result<(PathBuf, PathBuf)> {
4043
let module_root = self.axl_deps_root.join(module_name);
41-
let resolved_script_path = module_root.join(module_subpath);
44+
45+
let mut resolved_script_path = module_root.clone();
46+
for component in module_subpath.components() {
47+
match component {
48+
Component::CurDir => {}
49+
Component::ParentDir => {
50+
resolved_script_path.pop();
51+
}
52+
Component::Normal(s) => {
53+
resolved_script_path.push(s);
54+
}
55+
_ => {
56+
return Err(starlark::Error::new_other(anyhow!(
57+
"invalid path component in load path: {}",
58+
module_subpath.display()
59+
)));
60+
}
61+
}
62+
}
63+
64+
if !resolved_script_path.starts_with(&module_root) {
65+
return Err(starlark::Error::new_other(anyhow!(
66+
"resolved path {} for load path {} escapes the module root directory {}",
67+
resolved_script_path.display(),
68+
module_subpath.display(),
69+
module_root.display()
70+
)));
71+
}
72+
4273
if resolved_script_path.is_file() {
4374
return Ok((resolved_script_path, module_root));
4475
}
4576
if !module_root.exists() {
4677
return Err(starlark::Error::new_other(anyhow!(
47-
"failed to resolve load(\"@{}/{}\", ...): module '{}' not found (expected module directory at '{}')",
78+
"failed to resolve load(\"@{}//{}\", ...): module '{}' not found (expected module directory at '{}')",
4879
module_name,
4980
module_subpath.display(),
5081
module_name,
5182
module_root.display()
5283
)));
5384
} else if !module_root.is_dir() {
5485
return Err(starlark::Error::new_other(anyhow!(
55-
"failed to resolve load(\"@{}/{}\", ...): module '{}' root at '{}' exists but is not a directory",
86+
"failed to resolve load(\"@{}//{}\", ...): module '{}' root at '{}' exists but is not a directory",
5687
module_name,
5788
module_subpath.display(),
5889
module_name,
5990
module_root.display()
6091
)));
6192
} else {
6293
return Err(starlark::Error::new_other(anyhow!(
63-
"failed to resolve load(\"@{}/{}\", ...): script file not found in module '{}' (expected at '{}')",
94+
"failed to resolve load(\"@{}//{}\", ...): script file not found in module '{}' (expected at '{}')",
6495
module_name,
6596
module_subpath.display(),
6697
module_name,
@@ -69,20 +100,7 @@ impl<'a> AxlLoader<'a> {
69100
}
70101
}
71102

72-
fn resolve_axl_script(&self, script_path: &Path) -> starlark::Result<PathBuf> {
73-
let script_path_str = script_path.to_str().ok_or_else(|| {
74-
starlark::Error::new_other(anyhow!(
75-
"path is not valid UTF-8: {}",
76-
script_path.display()
77-
))
78-
})?;
79-
let base: &PathBuf =
80-
if script_path_str.starts_with("./") || script_path_str.starts_with("../") {
81-
&self.script_dir
82-
} else {
83-
&self.module_root
84-
};
85-
103+
fn resolve_axl_script(&self, script_path: &Path, base: &PathBuf) -> starlark::Result<PathBuf> {
86104
let mut resolved_script_path = base.clone();
87105
for component in script_path.components() {
88106
match component {
@@ -125,38 +143,53 @@ impl<'a> FileLoader for AxlLoader<'a> {
125143
)));
126144
}
127145

128-
let (module_name, module_subpath_or_script_path) = sanitize_load_path_lexically(load_path)?;
129-
130-
// Check if the @module/path/to/file.axl has been loaded before; if it has use the cached version.
131-
// NB: it is by design that a `root/sub/.aspect/modules/foo/foo.axl` can override
132-
// the load of a `root/.aspect/modules/foo/foo.axl` and of `modules_cache/foo/foo.axl` since
133-
// we want to allow overriding of individual files in modules as needed.
134-
if module_name.is_some() {
135-
let module_specifier = format!(
136-
"@{}/{}",
137-
module_name.as_ref().unwrap(),
138-
module_subpath_or_script_path.display()
139-
);
140-
if let Some(module) =
141-
LOADED_MODULES.with(|modules| modules.borrow().get(&module_specifier).cloned())
142-
{
143-
return Ok(module);
146+
let load_path_enum = sanitize_load_path_lexically(load_path)?;
147+
148+
let (resolved_script_path, module_root) = match &load_path_enum {
149+
LoadPath::ModuleSpecifier { module, subpath } => {
150+
self.resolve_axl_module_script(module, subpath)?
144151
}
152+
LoadPath::ModuleSubpath(subpath) => (
153+
self.resolve_axl_script(subpath, &self.module_root)?,
154+
self.module_root.clone(),
155+
),
156+
LoadPath::RelativePath(relpath) => (
157+
self.resolve_axl_script(relpath, &self.script_dir)?,
158+
self.module_root.clone(),
159+
),
160+
};
161+
162+
let target_module_name = match &load_path_enum {
163+
LoadPath::ModuleSpecifier { module, .. } => module.clone(),
164+
_ => self.module_name.clone(),
165+
};
166+
167+
if target_module_name.is_empty() {
168+
return Err(starlark::Error::new_other(anyhow!(
169+
"failed to resolve a target module name from load path {}",
170+
load_path
171+
)));
145172
}
146173

147-
// Resolve the load path to a file on disk
148-
let (resolved_script_path, module_root) = if module_name.is_some() {
149-
self.resolve_axl_module_script(
150-
module_name.as_ref().unwrap().as_str(),
151-
&module_subpath_or_script_path,
152-
)?
153-
} else {
154-
(
155-
self.resolve_axl_script(&module_subpath_or_script_path)?,
156-
self.module_root.clone(),
157-
)
174+
let module_specifier = {
175+
let rel = resolved_script_path
176+
.strip_prefix(&module_root)
177+
.map_err(|e| {
178+
starlark::Error::new_other(anyhow!("failed to strip prefix: {}", e))
179+
})?;
180+
let subpath_str = rel
181+
.to_str()
182+
.ok_or_else(|| starlark::Error::new_other(anyhow!("non-UTF8 path")))?
183+
.replace('\\', "/");
184+
format!("@{}//{}", target_module_name, subpath_str)
158185
};
159186

187+
if let Some(cached_module) =
188+
LOADED_MODULES.with(|modules| modules.borrow().get(&module_specifier).cloned())
189+
{
190+
return Ok(cached_module);
191+
}
192+
160193
// Cycle detection: Prevent loading the same file recursively.
161194
let mut cycle_error = None;
162195
LOAD_STACK.with(|stack| {
@@ -197,6 +230,7 @@ impl<'a> FileLoader for AxlLoader<'a> {
197230
.parent()
198231
.expect("file path has parent")
199232
.to_path_buf(),
233+
module_name: target_module_name,
200234
module_root,
201235
axl_deps_root: self.axl_deps_root.clone(),
202236
};
@@ -207,19 +241,12 @@ impl<'a> FileLoader for AxlLoader<'a> {
207241
drop(eval);
208242
let frozen_module = module.freeze()?;
209243

210-
// Cache the load @module/path/to/file.axl so it can be re-used on subsequent loads
211-
if module_name.is_some() {
212-
let module_specifier = format!(
213-
"@{}/{}",
214-
module_name.as_ref().unwrap(),
215-
module_subpath_or_script_path.display()
216-
);
217-
LOADED_MODULES.with(|modules| {
218-
modules
219-
.borrow_mut()
220-
.insert(module_specifier, frozen_module.clone());
221-
});
222-
}
244+
// Cache the load @module//path/to/file.axl so it can be re-used on subsequent loads
245+
LOADED_MODULES.with(|modules| {
246+
modules
247+
.borrow_mut()
248+
.insert(module_specifier, frozen_module.clone());
249+
});
223250

224251
// Pop the load stack after successful load
225252
LOAD_STACK.with(|stack| {

crates/axl-runtime/src/eval/mod.rs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ use crate::engine::task_args::TaskArgs;
1919
use crate::engine::task_context::TaskContext;
2020
use crate::engine::{self, task::Task};
2121
use crate::eval::load::AxlLoader;
22-
use crate::helpers::{normalize_abs_path_lexically, sanitize_load_path_lexically};
22+
use crate::helpers::{normalize_abs_path_lexically, sanitize_load_path_lexically, LoadPath};
23+
use crate::module::AXL_ROOT_MODULE_NAME;
2324

2425
/// The core evaluator for .axl files, holding configuration like module root,
2526
/// Starlark dialect, globals, and store. Used to evaluate .axl files securely.
@@ -214,19 +215,22 @@ impl AxlScriptEvaluator {
214215
/// the evaluated script or an error. Performs security checks to ensure the script
215216
/// file is within the module root.
216217
pub fn eval(&self, script_path: &Path) -> Result<EvaluatedAxlScript, EvalError> {
217-
let (module_name, script_path) =
218-
sanitize_load_path_lexically(script_path.to_str().unwrap())?;
218+
let script_path = sanitize_load_path_lexically(script_path.to_str().unwrap())?;
219219

220-
// Don't allow evaluating script paths starting with @module names.
221-
if module_name.is_some() {
222-
return Err(EvalError::UnknownError(anyhow::anyhow!(
223-
"axl scripts cannot be loaded directly from a module (load path starts with '@'): {}",
224-
script_path.display(),
225-
)));
226-
}
220+
let script_subpath = match script_path {
221+
LoadPath::ModuleSpecifier { module, subpath } => {
222+
return Err(EvalError::UnknownError(anyhow::anyhow!(
223+
"axl scripts cannot be loaded directly from a module (load path starts with '@'): @{}//{}",
224+
module,
225+
subpath.display(),
226+
)));
227+
}
228+
LoadPath::ModuleSubpath(subpath) | LoadPath::RelativePath(subpath) => subpath,
229+
};
227230

228231
// Ensure that we're not evaluating a script outside of the module root
229-
let abs_script_path = normalize_abs_path_lexically(&self.module_root.join(&script_path))?;
232+
let abs_script_path =
233+
normalize_abs_path_lexically(&self.module_root.join(&script_subpath))?;
230234
if !abs_script_path.starts_with(&self.module_root) {
231235
return Err(EvalError::UnknownError(anyhow::anyhow!(
232236
"axl script path {} resolves outside the module root {}",
@@ -242,6 +246,7 @@ impl AxlScriptEvaluator {
242246
.parent()
243247
.expect("file path has parent")
244248
.to_path_buf(),
249+
module_name: AXL_ROOT_MODULE_NAME.to_string(),
245250
module_root: self.module_root.clone(),
246251
axl_deps_root: self.axl_deps_root.clone(),
247252
};
@@ -266,7 +271,7 @@ impl AxlScriptEvaluator {
266271
Ok(EvaluatedAxlScript::new(
267272
abs_script_path,
268273
self.module_name.clone(),
269-
script_path.as_os_str().to_string_lossy().to_string(),
274+
script_subpath.display().to_string(),
270275
self.store.clone(),
271276
module,
272277
))

0 commit comments

Comments
 (0)