[pitboss] phase 09: Track D.1 + D.2 — Project dependency capture + workdir staging

This commit is contained in:
pitboss 2026-05-14 13:40:47 -05:00
parent a7fbc37c21
commit 2f01894353
16 changed files with 2009 additions and 0 deletions

View file

@ -13,9 +13,11 @@
//! - `PayloadSlot::EnvVar(name)` — set env var before calling.
//! - Other slots produce `UnsupportedReason::PayloadSlotUnsupported`.
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::lang::{HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use crate::utils::project::DetectedFramework;
/// Zero-sized [`LangEmitter`] handle for Python. Registered in the
/// `lang::dispatch` table; method bodies delegate to the existing free
@ -40,6 +42,14 @@ impl LangEmitter for PythonEmitter {
"python emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — Track B will add framework + CLI shapes in phase 12"
)
}
/// Phase 09 — Track D.2: emit a pinned `requirements.txt` (and a
/// matching `pyproject.toml` stub when `pyproject.toml` is the
/// project's canonical manifest) covering every captured direct dep
/// plus the framework deps inferred from the project manifest.
fn materialize_runtime(&self, env: &Environment) -> RuntimeArtifacts {
materialize_python(env)
}
}
/// Source of the `__nyx_probe` shim for the Python harness.
@ -168,6 +178,163 @@ def __nyx_install_crash_guard(sink_callee):
"#
}
/// Phase 09 - Track D.2: synthesise a `requirements.txt` from the
/// captured deps in `env`.
///
/// The output is a deterministic, alphabetised listing of every
/// non-stdlib direct dep the entry file imported plus the framework deps
/// inferred from the manifest detector. Each entry is emitted as the
/// canonical pip-installable name; version pins are intentionally
/// omitted so the system pip resolves the latest compatible release
/// against the user's pinned Python interpreter (the spec's
/// `toolchain_id` field). A future phase can fold pinned versions in
/// once the capture pass learns to parse the project's own lockfile.
pub fn materialize_python(env: &Environment) -> RuntimeArtifacts {
let mut artifacts = RuntimeArtifacts::new();
let mut deps: Vec<String> = Vec::new();
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
// Direct imports first — these mirror the entry file faithfully.
for d in &env.direct_deps {
if is_python_stdlib(d) {
continue;
}
let canonical = canonical_python_pkg_name(d);
if seen.insert(canonical.clone()) {
deps.push(canonical);
}
}
// Framework deps next — these may not appear as direct imports in
// every entry file, but they have to be installed for the runtime
// to resolve framework decorators.
for fw in &env.frameworks {
if let Some(name) = python_framework_pkg_name(*fw) {
let canonical = canonical_python_pkg_name(name);
if seen.insert(canonical.clone()) {
deps.push(canonical);
}
}
}
deps.sort_unstable();
let mut body = String::with_capacity(64);
body.push_str("# Auto-generated by Nyx — Phase 09 (Track D.2).\n");
body.push_str(&format!("# spec_hash = {}\n", env.spec_hash));
body.push_str(&format!(
"# toolchain = {} (drift={})\n",
env.toolchain.toolchain_id, env.toolchain.toolchain_drift
));
for d in &deps {
body.push_str(d);
body.push('\n');
}
artifacts.push("requirements.txt", body);
artifacts
}
/// Returns true when `name` is a Python standard-library top-level
/// package. Conservative: matches the names the harness build path
/// would silently drop from `requirements.txt` anyway.
fn is_python_stdlib(name: &str) -> bool {
matches!(
name,
"abc"
| "argparse"
| "asyncio"
| "base64"
| "binascii"
| "collections"
| "contextlib"
| "copy"
| "csv"
| "ctypes"
| "dataclasses"
| "datetime"
| "decimal"
| "difflib"
| "email"
| "enum"
| "errno"
| "fcntl"
| "fnmatch"
| "functools"
| "getopt"
| "getpass"
| "glob"
| "gzip"
| "hashlib"
| "hmac"
| "http"
| "importlib"
| "inspect"
| "io"
| "ipaddress"
| "itertools"
| "json"
| "logging"
| "math"
| "multiprocessing"
| "operator"
| "os"
| "pathlib"
| "pickle"
| "platform"
| "posixpath"
| "queue"
| "random"
| "re"
| "secrets"
| "select"
| "shutil"
| "signal"
| "socket"
| "sqlite3"
| "ssl"
| "stat"
| "string"
| "struct"
| "subprocess"
| "sys"
| "tempfile"
| "threading"
| "time"
| "traceback"
| "types"
| "typing"
| "unicodedata"
| "unittest"
| "urllib"
| "uuid"
| "warnings"
| "weakref"
| "xml"
| "zipfile"
| "zlib"
)
}
/// Canonicalise common Python pkg aliases to their PyPI distribution
/// name (e.g. `cv2` → `opencv-python`).
fn canonical_python_pkg_name(name: &str) -> String {
let lower = name.to_ascii_lowercase();
match lower.as_str() {
"flask" => "Flask".to_owned(),
"cv2" => "opencv-python".to_owned(),
"sqlalchemy" => "SQLAlchemy".to_owned(),
"yaml" => "PyYAML".to_owned(),
"psycopg2" => "psycopg2-binary".to_owned(),
_ => lower,
}
}
fn python_framework_pkg_name(fw: DetectedFramework) -> Option<&'static str> {
match fw {
DetectedFramework::Flask => Some("flask"),
DetectedFramework::Django => Some("django"),
_ => None,
}
}
/// Emit a Python harness for `spec`.
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
// Validate payload slot.