Skip to content

Commit a0c56c2

Browse files
Package manager: omc --install + omc.toml + omc_modules/
OMC has a working package manager. The pieces: * `omc.toml` at project root — declares dependencies as URLs: [dependencies] np = "https://example.com/raw/np.omc" pd = "https://example.com/raw/pd.omc" * `omc --install` — reads the manifest, fetches every URL, writes to `omc_modules/<name>.omc`. Installation is idempotent (re-run to update). * `omc --install <URL>` — single-shot install of a specific URL. Module name derived from the URL basename (sans `.omc`). * `omc --list` — enumerates installed modules. * `import "np";` resolution — interpreter.rs now puts `omc_modules/` FIRST in the search path, before OMC_STDLIB_PATH and the legacy Python OMC stdlib paths. Mirrors npm's node_modules / pip's site-packages convention. Implementation: * HTTP fetch via embedded Python `requests` (no new Rust dep). * TOML parse via embedded Python `tomllib` (Python 3.11+ stdlib). * Both use the always-on pyo3 layer landed in the previous commit. This is "eat our own dogfood" — OMC's package manager is itself proof that the integration story works. Zero new Rust crates. New OMC builtin: `py_fetch_text(url)` — convenience GET via embedded requests, returns body string. Useful for any OMC program that needs to pull content from the web. Verified end-to-end: - Spun up a local http.server pointing at examples/lib/ - omc.toml with three dependencies - `omc --install` fetched all three to omc_modules/ - `omc --list` enumerated them - `import "np";` resolved from omc_modules/ - `np.pi`, `np.mean`, sk.load_iris all worked Help text updated. 43/43 functional examples produce identical output under tree-walk and VM. 92/92 unit tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent e6065b9 commit a0c56c2

3 files changed

Lines changed: 259 additions & 2 deletions

File tree

omnimcode-core/src/interpreter.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ impl Interpreter {
173173
/// small built-in list that includes the canonical Python OMC stdlib.
174174
fn module_search_path() -> Vec<std::path::PathBuf> {
175175
let mut paths = Vec::new();
176+
// Project-local package cache. Populated by `omc --install`
177+
// and checked first so `import "np";` resolves the local
178+
// copy before falling back to user paths or the legacy stdlib.
179+
// Mirrors npm's node_modules / pip's site-packages convention.
180+
paths.push(std::path::PathBuf::from("omc_modules"));
176181
if let Ok(env) = std::env::var("OMC_STDLIB_PATH") {
177182
for p in env.split(':') {
178183
if !p.is_empty() {

omnimcode-core/src/main.rs

Lines changed: 144 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@ use std::io::{self, Write};
1111
fn main() {
1212
let args: Vec<String> = env::args().collect();
1313

14-
// Parse simple flag-style args: --check, --fmt, --help / -h
15-
// Anything else after the flag is the input file.
14+
// Parse simple flag-style args. Anything else is the input file
15+
// (or the install spec when --install is set).
1616
let mut mode = "run";
1717
let mut file_arg: Option<&str> = None;
1818
for a in args.iter().skip(1) {
1919
match a.as_str() {
2020
"--check" | "-c" => mode = "check",
2121
"--fmt" | "--format" | "-f" => mode = "fmt",
22+
"--install" | "-i" => mode = "install",
23+
"--list" | "-l" => mode = "list",
2224
"--help" | "-h" => mode = "help",
2325
other if !other.starts_with('-') => file_arg = Some(other),
2426
other => {
@@ -50,6 +52,8 @@ fn main() {
5052
eprintln!("--fmt requires a file argument.");
5153
2
5254
}
55+
("install", spec) => install_command(spec),
56+
("list", _) => list_command(),
5357
_ => unreachable!(),
5458
};
5559
if exit_code != 0 {
@@ -72,14 +76,151 @@ fn maybe_register_python(interp: &mut Interpreter) {
7276
omnimcode_core::python_embed::register_python_builtins(interp);
7377
}
7478

79+
/// `--install [URL_OR_NAME]`. With no argument: read `omc.toml` in
80+
/// the current directory and install every entry in [dependencies].
81+
/// With a URL: fetch it and store the file under `omc_modules/`,
82+
/// using the basename (sans .omc) as the module name. With a name
83+
/// that doesn't look like a URL: error (no central registry yet —
84+
/// users provide explicit URLs in omc.toml).
85+
///
86+
/// Eats our own dogfood: uses the embedded Python `requests` for
87+
/// the HTTP fetch and `tomllib` for the manifest parse. Zero new
88+
/// Rust dependencies.
89+
fn install_command(spec: Option<&str>) -> i32 {
90+
use omnimcode_core::python_embed::{install_url_via_python, parse_omc_toml_via_python};
91+
92+
if std::env::var("OMC_NO_PYTHON").as_deref() == Ok("1") {
93+
eprintln!("--install requires Python (used for HTTP fetch + TOML parse).");
94+
eprintln!("Unset OMC_NO_PYTHON or run with Python embedding enabled.");
95+
return 2;
96+
}
97+
98+
// Ensure omc_modules/ exists.
99+
if let Err(e) = std::fs::create_dir_all("omc_modules") {
100+
eprintln!("install: cannot create omc_modules/: {}", e);
101+
return 1;
102+
}
103+
104+
match spec {
105+
Some(spec) => {
106+
let url = if spec.starts_with("http://") || spec.starts_with("https://") {
107+
spec.to_string()
108+
} else {
109+
eprintln!("install: argument must be a URL (no central registry yet).");
110+
eprintln!(" For a manifest install, run `omc --install` with no arg");
111+
eprintln!(" and create an omc.toml in this directory.");
112+
return 2;
113+
};
114+
// Derive name from URL basename.
115+
let name = url
116+
.rsplit('/')
117+
.next()
118+
.unwrap_or("module")
119+
.trim_end_matches(".omc");
120+
match install_url_via_python(name, &url) {
121+
Ok(path) => {
122+
println!("installed: {} -> {}", name, path);
123+
0
124+
}
125+
Err(e) => {
126+
eprintln!("install({}): {}", name, e);
127+
1
128+
}
129+
}
130+
}
131+
None => {
132+
let manifest_path = "omc.toml";
133+
let manifest_text = match std::fs::read_to_string(manifest_path) {
134+
Ok(t) => t,
135+
Err(e) => {
136+
eprintln!("install: cannot read {}: {}", manifest_path, e);
137+
eprintln!(" Create one with [dependencies] entries:");
138+
eprintln!("");
139+
eprintln!(" [dependencies]");
140+
eprintln!(" np = \"https://raw.githubusercontent.com/.../np.omc\"");
141+
return 1;
142+
}
143+
};
144+
let deps = match parse_omc_toml_via_python(&manifest_text) {
145+
Ok(d) => d,
146+
Err(e) => {
147+
eprintln!("install: omc.toml parse: {}", e);
148+
return 1;
149+
}
150+
};
151+
if deps.is_empty() {
152+
println!("install: no [dependencies] in omc.toml — nothing to do.");
153+
return 0;
154+
}
155+
let mut failures = 0;
156+
for (name, url) in &deps {
157+
match install_url_via_python(name, url) {
158+
Ok(path) => println!("installed: {} -> {}", name, path),
159+
Err(e) => {
160+
eprintln!("install({}): {}", name, e);
161+
failures += 1;
162+
}
163+
}
164+
}
165+
if failures > 0 { 1 } else { 0 }
166+
}
167+
}
168+
}
169+
170+
/// `--list`: enumerate everything in omc_modules/.
171+
fn list_command() -> i32 {
172+
let dir = std::path::Path::new("omc_modules");
173+
if !dir.exists() {
174+
println!("(no omc_modules/ in this directory)");
175+
return 0;
176+
}
177+
let entries = match std::fs::read_dir(dir) {
178+
Ok(e) => e,
179+
Err(e) => {
180+
eprintln!("list: cannot read omc_modules/: {}", e);
181+
return 1;
182+
}
183+
};
184+
let mut names: Vec<String> = entries
185+
.filter_map(|e| e.ok())
186+
.filter_map(|e| {
187+
let p = e.path();
188+
if p.extension().and_then(|s| s.to_str()) == Some("omc") {
189+
p.file_stem()
190+
.and_then(|s| s.to_str())
191+
.map(|s| s.to_string())
192+
} else {
193+
None
194+
}
195+
})
196+
.collect();
197+
names.sort();
198+
if names.is_empty() {
199+
println!("(no installed modules — use --install to add some)");
200+
} else {
201+
for n in names {
202+
println!(" {}", n);
203+
}
204+
}
205+
0
206+
}
207+
75208
fn print_help() {
76209
let prog = env::args().next().unwrap_or_else(|| "omnimcode-standalone".to_string());
77210
println!("Usage:");
78211
println!(" {} [FILE] run a program (or start REPL if no file)", prog);
79212
println!(" {} --check FILE run heal pass, print diagnostics, exit", prog);
80213
println!(" {} --fmt FILE pretty-print AST as canonical OMC source", prog);
214+
println!(" {} --install [URL] install package from URL into omc_modules/", prog);
215+
println!(" (no URL = read omc.toml [dependencies])", );
216+
println!(" {} --list list packages installed under omc_modules/", prog);
81217
println!(" {} --help this message", prog);
82218
println!();
219+
println!("omc.toml format (for --install with no arg):");
220+
println!(" [dependencies]");
221+
println!(" np = \"https://example.com/raw/np.omc\"");
222+
println!(" pd = \"https://example.com/raw/pd.omc\"");
223+
println!();
83224
println!("Environment variables:");
84225
println!(" OMC_VM=1 execute through the Rust bytecode VM");
85226
println!(" OMC_HEAL=1 auto-heal AST before execution (iterative)");
@@ -89,6 +230,7 @@ fn print_help() {
89230
println!(" OMC_OPT=0 disable optimizer (on by default)");
90231
println!(" OMC_OPT_STATS=1 print optimizer pass statistics");
91232
println!(" OMC_STDLIB_PATH=... colon-separated module search path");
233+
println!(" OMC_NO_PYTHON=1 skip embedded CPython initialisation");
92234
}
93235

94236
fn read_and_run(path: &str) -> Result<(), String> {

omnimcode-core/src/python_embed.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,20 @@ pub fn register_python_builtins(interp: &mut Interpreter) {
424424
Ok(Value::Null)
425425
});
426426

427+
// ---- py_fetch_text(url) -> string -------------------------------
428+
// Convenience: HTTP GET via embedded Python `requests`. Returns
429+
// body as string on 2xx, errors on anything else. Used internally
430+
// by `omc --install` so we don't need a separate Rust HTTP crate.
431+
interp.register_builtin("py_fetch_text", |args| {
432+
if args.is_empty() {
433+
return Err("py_fetch_text requires (url)".to_string());
434+
}
435+
let url = args[0].to_display_string();
436+
let body = fetch_url(&url)
437+
.map_err(|e| format!("py_fetch_text({}): {}", url, e))?;
438+
Ok(Value::String(body))
439+
});
440+
427441
// ---- py_callback("omc_fn_name") -> handle (Python callable) -------
428442
// Returns a Python callable that, when invoked from Python with
429443
// positional args, calls back into OMC's `omc_fn_name` with the
@@ -501,3 +515,99 @@ impl OmcCallback {
501515
format!("<OmcCallback '{}'>", self.fn_name)
502516
}
503517
}
518+
519+
// ===========================================================================
520+
// Package manager helpers — used by `omc --install` from main.rs.
521+
//
522+
// We do the HTTP fetch and TOML parse via embedded Python (`requests`
523+
// + `tomllib`) rather than pulling in Rust HTTP/TOML crates. The
524+
// dependency model is already "Python is always on" — leaning on it
525+
// for tooling avoids dep bloat and proves the integration works for
526+
// our own infrastructure.
527+
// ===========================================================================
528+
529+
/// HTTP GET via embedded Python's `requests`. Returns the response
530+
/// body on 2xx; Err on connection failure, non-2xx, or a missing
531+
/// `requests` install.
532+
pub fn fetch_url(url: &str) -> Result<String, String> {
533+
Python::with_gil(|py| {
534+
let requests = py
535+
.import_bound("requests")
536+
.map_err(|e| format!("requests not installed: {}", e))?;
537+
let response = requests
538+
.call_method1("get", (url,))
539+
.map_err(|e| format!("GET failed: {}", e))?;
540+
let status: u16 = response
541+
.getattr("status_code")
542+
.and_then(|s| s.extract())
543+
.map_err(|e| format!("status_code: {}", e))?;
544+
if !(200..300).contains(&status) {
545+
return Err(format!("HTTP {}", status));
546+
}
547+
let body: String = response
548+
.getattr("text")
549+
.and_then(|t| t.extract())
550+
.map_err(|e| format!("read body: {}", e))?;
551+
Ok(body)
552+
})
553+
}
554+
555+
/// Fetch `url` and write to `omc_modules/<name>.omc`. Returns the
556+
/// final on-disk path on success.
557+
pub fn install_url_via_python(name: &str, url: &str) -> Result<String, String> {
558+
let body = fetch_url(url)?;
559+
let path = format!("omc_modules/{}.omc", name);
560+
std::fs::write(&path, body).map_err(|e| format!("write {}: {}", path, e))?;
561+
Ok(path)
562+
}
563+
564+
/// Parse `omc.toml`'s `[dependencies]` table via Python's `tomllib`
565+
/// (stdlib in 3.11+). Returns a list of (name, url) pairs preserving
566+
/// source order.
567+
pub fn parse_omc_toml_via_python(text: &str) -> Result<Vec<(String, String)>, String> {
568+
Python::with_gil(|py| {
569+
let tomllib = py
570+
.import_bound("tomllib")
571+
.map_err(|e| format!("tomllib not available (need Python 3.11+): {}", e))?;
572+
// tomllib.loads(text) — needs bytes in some versions, str in others.
573+
// Use loads with str, fall back to bytes.
574+
let parsed = match tomllib.call_method1("loads", (text,)) {
575+
Ok(v) => v,
576+
Err(_) => tomllib
577+
.call_method1("loads", (text.as_bytes(),))
578+
.map_err(|e| format!("tomllib.loads: {}", e))?,
579+
};
580+
let dict = parsed
581+
.downcast::<PyDict>()
582+
.map_err(|e| format!("toml root must be a table: {}", e))?;
583+
let deps_obj = match dict.get_item("dependencies") {
584+
Ok(Some(o)) => o,
585+
_ => return Ok(Vec::new()),
586+
};
587+
let deps = deps_obj
588+
.downcast::<PyDict>()
589+
.map_err(|e| format!("[dependencies] must be a table: {}", e))?;
590+
let mut out: Vec<(String, String)> = Vec::with_capacity(deps.len());
591+
for (k, v) in deps.iter() {
592+
let name: String = k.extract().map_err(|e| format!("dep name: {}", e))?;
593+
// Accept either a string URL or a table with `url = "..."`.
594+
let url: String = if let Ok(s) = v.extract::<String>() {
595+
s
596+
} else if let Ok(t) = v.downcast::<PyDict>() {
597+
match t.get_item("url") {
598+
Ok(Some(u)) => u
599+
.extract::<String>()
600+
.map_err(|e| format!("dep {} url: {}", name, e))?,
601+
_ => return Err(format!("dep {} table missing `url`", name)),
602+
}
603+
} else {
604+
return Err(format!(
605+
"dep {} must be a string URL or table with `url`",
606+
name
607+
));
608+
};
609+
out.push((name, url));
610+
}
611+
Ok(out)
612+
})
613+
}

0 commit comments

Comments
 (0)