diff --git a/.ghjk/lock.json b/.ghjk/lock.json index 0273092..8f5ab19 100644 --- a/.ghjk/lock.json +++ b/.ghjk/lock.json @@ -537,7 +537,6 @@ "ghjkEnvProvInstSet____rust": { "installs": [ "bciqikjfnbntvagpghawbzlfp2es6lnqzhba3qx5de7tdrmvhuzhsjqa", - "bciqfrfun7z7soj7yxzziyvmt2jnebqvneeoozk5vynmg5pa6wqynhvi", "bciqlmoqot4jk2lb2b34pldr5iiwsfm3biuipzesjkkwmc2n2o6nlw4q", "bciqe72molvtvcuj3tuh47ziue2oqd6t4qetxn3rsoa764ofup6uwjmi", "bciqe4zlekl4uqqbhxunac7br24mrf6cdpfrfblahqa4vrgaqjujcl4i", @@ -552,7 +551,6 @@ "bciqhsrsmayibhhhcp3jmay4tnrsyhz5od6ngtaazymx3o64xxzbqiha", "bciqasgft3ee3shnt2sivmethcza5cebrudgfyw5ntqetu5jnuniqblq", "bciqikjfnbntvagpghawbzlfp2es6lnqzhba3qx5de7tdrmvhuzhsjqa", - "bciqfrfun7z7soj7yxzziyvmt2jnebqvneeoozk5vynmg5pa6wqynhvi", "bciqlmoqot4jk2lb2b34pldr5iiwsfm3biuipzesjkkwmc2n2o6nlw4q", "bciqe72molvtvcuj3tuh47ziue2oqd6t4qetxn3rsoa764ofup6uwjmi", "bciqe4zlekl4uqqbhxunac7br24mrf6cdpfrfblahqa4vrgaqjujcl4i", @@ -579,6 +577,7 @@ "test": { "ty": "denoFile@v1", "key": "test", + "workingDir": "./src", "envKey": "bciqonerdglss7wgwanl5kmaobd65m62hhjbttwhi6kdy5sqf6us6b4a" }, "lock-sed": { @@ -1156,40 +1155,6 @@ "moduleSpecifier": "file:///ports/protoc.ts" } }, - "bciqfrfun7z7soj7yxzziyvmt2jnebqvneeoozk5vynmg5pa6wqynhvi": { - "port": { - "ty": "denoWorker@v1", - "name": "pipi_pypi", - "platforms": [ - "x86_64-linux", - "aarch64-linux", - "x86_64-darwin", - "aarch64-darwin", - "x86_64-windows", - "aarch64-windows", - "x86_64-freebsd", - "aarch64-freebsd", - "x86_64-netbsd", - "aarch64-netbsd", - "x86_64-aix", - "aarch64-aix", - "x86_64-solaris", - "aarch64-solaris", - "x86_64-illumos", - "aarch64-illumos", - "x86_64-android", - "aarch64-android" - ], - "version": "0.1.0", - "buildDeps": [ - { - "name": "cpy_bs_ghrel" - } - ], - "moduleSpecifier": "file:///ports/pipi.ts" - }, - "packageName": "cmake" - }, "bciqlmoqot4jk2lb2b34pldr5iiwsfm3biuipzesjkkwmc2n2o6nlw4q": { "version": "v2.4.0", "port": { diff --git a/flake.lock b/flake.lock index 134adc1..cdf2e6c 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1753250450, - "narHash": "sha256-i+CQV2rPmP8wHxj0aq4siYyohHwVlsh40kV89f3nw1s=", + "lastModified": 1755186698, + "narHash": "sha256-wNO3+Ks2jZJ4nTHMuks+cxAiVBGNuEBXsT29Bz6HASo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "fc02ee70efb805d3b2865908a13ddd4474557ecf", + "rev": "fbcf476f790d8a217c3eab4e12033dc4a0f6d23c", "type": "github" }, "original": { @@ -62,11 +62,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1753411536, - "narHash": "sha256-lm88KTYlhsh5usJLGlWQbG4HWWr2FdO26TSssCw6Wdg=", + "lastModified": 1755571033, + "narHash": "sha256-V8gmZBfMiFGCyGJQx/yO81LFJ4d/I5Jxs2id96rLxrM=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "7ae12d14d6bb74acfadf31e17a204d928eaf77b8", + "rev": "95487740bb7ac11553445e9249041a6fa4b5eccf", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 74bd890..0188930 100644 --- a/flake.nix +++ b/flake.nix @@ -32,6 +32,7 @@ buildInputs = with pkgs; [ rustChannel clang + cmake llvmPackages.libclang pkg-config sqlite diff --git a/ghjk.ts b/ghjk.ts index be447fe..438847e 100644 --- a/ghjk.ts +++ b/ghjk.ts @@ -51,7 +51,6 @@ ghjk.env("_rust") .install( // install rust using rustup or nix (there's a flake.nix file) ports.protoc(), - ports.pipi({ packageName: "cmake" })[0], ...(Deno.build.os == "linux" && !Deno.env.has("NO_MOLD") ? [ports.mold({ version: "v2.4.0", @@ -208,6 +207,10 @@ ghjk.task( { inherit: false }, ); -ghjk.task(function test($) { - console.log($.argv); +ghjk.task(async function test($) { + console.log($.workingDir, "custom"); + await $`echo $PWD`; + console.log(Deno.cwd(), "posix"); +}, { + workingDir: "./src", }); diff --git a/install.sh b/install.sh index 4b55260..8e67dd2 100755 --- a/install.sh +++ b/install.sh @@ -2,6 +2,9 @@ # shellcheck disable=SC2016 # shellcheck disable=SC2028 +# FIXME: move to .local/bin only after install.ts succeeds +# FIXME: when detecting nixos, advise appropriately + set -e -u if ! command -v curl >/dev/null; then @@ -126,7 +129,7 @@ EOF fi fi -GHJK_INSTALLER_URL="${GHJK_INSTALLER_URL:-https://raw.github.com/$ORG/$REPO/$VERSION/install.ts}" +GHJK_INSTALLER_URL="${GHJK_INSTALLER_URL:-https://raw.githubusercontent.com/$ORG/$REPO/$VERSION/install.ts}" "$GHJK_INSTALL_EXE_DIR/$EXE" deno run -A "$GHJK_INSTALLER_URL" # Print shell-specific commands for the user to run manually, with the # current shell shown last for convenience. We do not modify any files. @@ -200,4 +203,4 @@ for sh in $ordered_shells; do done echo echo "ghjk has been installed to $GHJK_INSTALL_EXE_DIR" -echo "Add $GHJK_INSTALL_EXE_DIR to your PATH by running one of the commands shown above." \ No newline at end of file +echo "Add $GHJK_INSTALL_EXE_DIR to your PATH by running one of the commands shown above." diff --git a/src/ghjk/cli.rs b/src/ghjk/cli.rs index 09d137c..cd5b7c3 100644 --- a/src/ghjk/cli.rs +++ b/src/ghjk/cli.rs @@ -83,6 +83,13 @@ pub async fn cli() -> Res { }; let gcx = Arc::new(gcx); + // Handle plumbing fast-path before any dynamic CLI/system init + if let QuickCliResult::ExecDenoTask(json_path) = &quick_res { + let res = crate::systems::tasks::exec::exec_deno_task_from_json(&gcx, json_path).await; + deno_cx.terminate().await?; + return res.map(|_| ExitCode::SUCCESS); + } + // ready system contexts let (system_manifests, envs_ctx, deno_sys_cx) = { let (sys_envs, envs_ctx) = systems::envs::system(gcx.clone(), &ghjkdir_path).await?; @@ -169,6 +176,8 @@ pub async fn cli() -> Res { root_cmd = root_cmd.subcommand(cmd); } + // (plumbing command now lives in QuickCommands and is handled earlier) + // Register CLI completion reducer on envs with the fully-built root_cmd // FIXME: this means completions are always generated even if we're not // writing activator scripts @@ -222,6 +231,19 @@ pub async fn cli() -> Res { Ok(QuickCommands::Deno { .. }) => { unreachable!("deno_quick_cli will prevent this") } + Ok(QuickCommands::Plumbing { commands }) => match commands { + PlumbingCommands::ExecDenoTask { json } => { + let res = crate::systems::tasks::exec::exec_deno_task_from_json( + &gcx, + std::path::Path::new(&json), + ) + .await; + systems.write_lockfile_or_log().await; + deno_sys_cx.terminate().await?; + deno_cx.terminate().await?; + return res.map(|_| ExitCode::SUCCESS); + } + }, Err(err) => { let kind = err.kind(); use clap::error::ErrorKind; @@ -268,6 +290,7 @@ enum QuickCliResult { ClapErr(clap::Error), Completions(CompletionShell), Exit(ExitCode), + ExecDenoTask(std::path::PathBuf), } impl QuickCliResult { fn exit(self, cmd: Option<&mut clap::Command>) -> ExitCode { @@ -293,6 +316,7 @@ impl QuickCliResult { ExitCode::SUCCESS } QuickCliResult::Exit(_) => unreachable!("can't happen"), + QuickCliResult::ExecDenoTask(_) => unreachable!("handled earlier"), } } } @@ -333,6 +357,11 @@ async fn try_quick_cli(config: &Config) -> Res { } QuickCommands::Init { commands } => commands.action(config).await?, QuickCommands::Deno { .. } => unreachable!("deno quick cli will have prevented this"), + QuickCommands::Plumbing { commands } => match commands { + PlumbingCommands::ExecDenoTask { json } => { + return Ok(QuickCliResult::ExecDenoTask(std::path::PathBuf::from(json))); + } + }, } Ok(QuickCliResult::Exit(ExitCode::SUCCESS)) @@ -400,6 +429,23 @@ enum QuickCommands { #[arg(raw(true))] args: String, }, + /// Internal commands (subject to change) + #[clap(hide = true)] + Plumbing { + #[command(subcommand)] + commands: PlumbingCommands, + }, +} + +#[derive(clap::Subcommand, Debug)] +enum PlumbingCommands { + /// Execute a Deno-backed task from a JSON request file + #[clap(hide = true, name = "exec-deno-task")] + ExecDenoTask { + /// Path to the JSON request file + #[arg(value_name = "JSON_FILE", value_hint = clap::ValueHint::FilePath)] + json: String, + }, } /// TODO: keep more of this in deno next time it's updated diff --git a/src/ghjk/systems/tasks.rs b/src/ghjk/systems/tasks.rs index 39a4bc8..ccff0a6 100644 --- a/src/ghjk/systems/tasks.rs +++ b/src/ghjk/systems/tasks.rs @@ -1,6 +1,6 @@ use crate::interlude::*; -mod exec; +pub mod exec; mod reducers; pub mod types; diff --git a/src/ghjk/systems/tasks/exec.rs b/src/ghjk/systems/tasks/exec.rs index 8d49e6f..9bacdb9 100644 --- a/src/ghjk/systems/tasks/exec.rs +++ b/src/ghjk/systems/tasks/exec.rs @@ -177,7 +177,7 @@ pub async fn exec_task( merged_env.insert(k, v); } - // Execute task via Deno worker + // Execute task via subprocess to ensure POSIX cwd/env isolation match task_def { TaskDefHashed::DenoFileV1(def) => { let ghjkfile = gcx @@ -191,31 +191,85 @@ pub async fn exec_task( ghjkfile.parent().unwrap_or(Path::new(".")).to_path_buf() }; - // Prepare payload like TS execTaskDeno expects - - let payload = ExecTaskArgs { - key: &def.key, - argv: &args, - working_dir: working_dir.to_string_lossy().to_string(), - env_vars: &merged_env, - }; - - // Execute via our JS bindings module: - // - module: src/ghjk/systems/tasks/bindings.ts - // - export: execTaskDeno(ghjkfileUri, payload) + // Prepare request JSON for plumbing exec-deno-task let ghjkfile_canon_path: std::path::PathBuf = ghjkfile.canonicalize().unwrap_or(ghjkfile.clone()); - let ghjkfile_uri = url::Url::from_file_path(&ghjkfile_canon_path) - .map_err(|_| ferr!("invalid ghjkfile path for file URL"))? - .to_string(); - // Call exec_task_deno to execute the task - let task_output = exec_task_deno(gcx, &ghjkfile_uri, &payload) + #[derive(Serialize)] + #[serde(rename_all = "camelCase")] + struct Req<'a> { + ghjkfile: &'a std::path::Path, + payload: ReqPayload<'a>, + result_file: &'a std::path::Path, + } + #[derive(Serialize)] + #[serde(rename_all = "camelCase")] + struct ReqPayload<'a> { + key: &'a str, + argv: &'a [String], + working_dir: &'a str, + env_vars: &'a IndexMap, + } + + let result_file_path = task_env_dir.path().join("result.json"); + let request_file_path = task_env_dir.path().join("request.json"); + let working_dir_string = working_dir.to_string_lossy().to_string(); + let req = Req { + ghjkfile: &ghjkfile_canon_path, + payload: ReqPayload { + key: &def.key, + argv: &args, + working_dir: &working_dir_string, + env_vars: &merged_env, + }, + result_file: &result_file_path, + }; + + let req_bytes = serde_json::to_vec_pretty(&req).expect_or_log("json error"); + tokio::fs::write(&request_file_path, req_bytes) + .await + .wrap_err("error writing exec-deno-task request json")?; + + // Spawn subprocess: ghjk plumbing exec-deno-task + let mut cmd = tokio::process::Command::new(&gcx.exec_path); + cmd.arg("plumbing") + .arg("exec-deno-task") + .arg(&request_file_path) + .current_dir(&working_dir); + // Inherit current env and override with merged task env + for (k, v) in &merged_env { + cmd.env(k, v); + } + // Put child in its own process group/session (unix only) + // Note: process group/session isolation disabled to avoid signal issues in tests + let status = cmd + .status() .await - .wrap_err("error executing deno task")?; + .wrap_err("error running plumbing exec-deno-task")?; + if !status.success() { + eyre::bail!( + "exec-deno-task subprocess failed with status {:?}", + status.code() + ); + } - // Store the task output - output.insert(deno_task.key.clone(), task_output); + // Read result + let result_raw = tokio::fs::read(&result_file_path) + .await + .wrap_err("error reading exec-deno-task result file")?; + let resp: serde_json::Value = serde_json::from_slice(&result_raw) + .wrap_err("error parsing exec-deno-task result json")?; + + // Expect either { data } or { error } + if let Some(error) = resp.get("error") { + eyre::bail!( + "task execution failed: {}", + serde_json::to_string_pretty(error) + .unwrap_or_else(|_| format!("{:?}", error)) + ); + } + let data = resp.get("data").cloned().unwrap_or(serde_json::Value::Null); + output.insert(deno_task.key.clone(), data); } } @@ -259,40 +313,56 @@ pub async fn exec_task( Ok(output) } -#[derive(Serialize)] +#[derive(Deserialize)] #[serde(rename_all = "camelCase")] -struct ExecTaskArgs<'a> { - key: &'a str, - argv: &'a [String], +struct ExecTaskArgsCli { + key: String, + argv: Vec, working_dir: String, - env_vars: &'a IndexMap, + env_vars: IndexMap, } -/// Execute a deno task following the exact pattern from host/deno.rs -async fn exec_task_deno( - gcx: &GhjkCtx, - ghjkfile_uri: &str, - payload: &ExecTaskArgs<'_>, -) -> Res { +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ExecDenoTaskRequestCli { + ghjkfile: std::path::PathBuf, + payload: ExecTaskArgsCli, + result_file: std::path::PathBuf, +} + +/// Execute a deno task from a JSON request file (plumbing command backend) +pub async fn exec_deno_task_from_json(gcx: &GhjkCtx, json_path: &std::path::Path) -> Res<()> { + let raw = tokio::fs::read(json_path).await?; + let req: ExecDenoTaskRequestCli = + serde_json::from_slice(&raw).wrap_err("error parsing exec-deno-task json request")?; + + // Build module URL for the bindings let main_module = gcx .config .repo_root .join("src/ghjk/systems/tasks/bindings.ts") .wrap_err("repo url error")?; - let mut ext_conf = crate::ext::ExtConfig::new(); + // Prepare blackboard payload + let ghjkfile_canon_path: std::path::PathBuf = + req.ghjkfile.canonicalize().unwrap_or(req.ghjkfile.clone()); + let ghjkfile_uri = url::Url::from_file_path(&ghjkfile_canon_path) + .map_err(|_| ferr!("invalid ghjkfile path for file URL"))? + .to_string(); - ext_conf.blackboard = [ - // blackboard is used as communication means - // with the deno side of the code - ( - "args".into(), - json!({ - "uri": ghjkfile_uri, - "payload": payload, - }), - ), - ] + let mut ext_conf = crate::ext::ExtConfig::new(); + ext_conf.blackboard = [( + "args".into(), + json!({ + "uri": ghjkfile_uri, + "payload": { + "key": req.payload.key, + "argv": req.payload.argv, + "workingDir": req.payload.working_dir, + "envVars": req.payload.env_vars, + }, + }), + )] .into_iter() .collect::>() .into(); @@ -327,27 +397,33 @@ async fn exec_task_deno( .run() .await .wrap_err("error on run of task deno worker")?; - if exit_code != 0 { - eyre::bail!("non-zero exit code running deno task execution module"); - } - let (_, resp) = bb.remove("resp").expect_or_log("resp missing"); + let resp_json = if exit_code == 0 { + let (_, resp) = bb.remove("resp").expect_or_log("resp missing"); + resp + } else { + json!({ + "error": { + "message": "non-zero exit code running deno task execution module", + "code": exit_code, + } + }) + }; - #[derive(Deserialize)] - #[serde(untagged, rename_all = "lowercase")] - enum TaskResult { - Ok { data: serde_json::Value }, - Err { error: serde_json::Value }, + // Write response JSON to the result file path + let data = serde_json::to_vec_pretty(&resp_json).unwrap_or_else(|_| b"null".to_vec()); + if let Some(parent) = req.result_file.parent() { + tokio::fs::create_dir_all(parent).await.ok(); } + tokio::fs::write(&req.result_file, data) + .await + .wrap_err("error writing result_file")?; - let result: TaskResult = - serde_json::from_value(resp).wrap_err("error deserializing task result")?; - - match result { - TaskResult::Ok { data } => Ok(data), - TaskResult::Err { error } => Err(ferr!( - "task execution failed: {}", - serde_json::to_string_pretty(&error).unwrap_or_else(|_| format!("{:?}", error)) - )), + // Return non-zero if it was an error shape + if resp_json.get("error").is_some() { + eyre::bail!("task execution failed (see result file)"); } + Ok(()) } + +// (old in-process exec helpers removed; subprocess plumbing path is now used exclusively) diff --git a/src/hooks/hook.fish b/src/hooks/hook.fish index d8eec0a..26d4c69 100644 --- a/src/hooks/hook.fish +++ b/src/hooks/hook.fish @@ -1,13 +1,13 @@ function __ghjk_get_mtime_ts switch (uname -s | tr '[:upper:]' '[:lower:]') case "linux" - stat -c "%.Y" $argv + stat -c "%Y" $argv case "darwin" # darwin stat doesn't support ms since epoch so we bring out the big guns deno eval 'console.log((await Deno.stat(Deno.args[0])).mtime.getTime())' $argv # stat -f "%Sm" -t "%s" $argv case "*" - stat -c "%.Y" $argv + stat -c "%Y" $argv end end diff --git a/tests/tasks.ts b/tests/tasks.ts index ee8b6ee..844caff 100644 --- a/tests/tasks.ts +++ b/tests/tasks.ts @@ -197,6 +197,30 @@ env("main") ghjk sync main cat output.txt test (cat output.txt) = 'A#STATIC, B#DYNAMIC' +`, + }, + { + name: "posix_working_dir", + tasks: [{ + name: "test", + workingDir: "./src", + fn: async ($) => { + // Write both views to a file for robust assertion + const posix = await $`/bin/sh -c 'pwd'`.text(); + const deno = Deno.cwd(); + await $.path("wd_check.txt").writeText( + `POSIX:${posix.trim()}\nDENO:${deno}\n`, + ); + }, + }], + ePoint: `fish`, + stdin: ` +mkdir -p src +$GHJK_DATA_DIR/ghjk x test +set -l posix (cat src/wd_check.txt | string match 'POSIX:*') +set -l deno (cat src/wd_check.txt | string match 'DENO:*') +test (string match -r '.*/src$' $posix) != '' +test (string match -r '.*/src$' $deno) != '' `, }, ];