g1t/crates/runner/src/actions/paths.rs
| 1 | //! Where a job's files go. In g1t's sandbox (and in a container a |
| 2 | //! self-hosted runner starts) that is GitHub's layout under `/home/runner`. |
| 3 | //! A self-hosted runner running a job directly on its machine |
| 4 | //! (`g1t-runner run --no-docker`) sets `G1T_RUNNER_ROOT` to a folder of its |
| 5 | //! own, and every `/home/runner` path, the spec's included, moves there. |
| 6 | |
| 7 | use std::path::PathBuf; |
| 8 | |
| 9 | use serde_json::Value; |
| 10 | |
| 11 | /// GitHub's runner layout, as g1t's sandbox has it. |
| 12 | pub(crate) const HOME_RUNNER: &str = "/home/runner"; |
| 13 | |
| 14 | /// The folder standing in for `/home/runner`, if the job runs elsewhere. |
| 15 | pub(crate) fn root() -> Option<PathBuf> { |
| 16 | std::env::var_os("G1T_RUNNER_ROOT").filter(|root| !root.is_empty()).map(PathBuf::from) |
| 17 | } |
| 18 | |
| 19 | /// `path` (under `/home/runner`) where this job keeps it. |
| 20 | pub(crate) fn under_home(path: &str) -> PathBuf { |
| 21 | match (root(), path.strip_prefix(HOME_RUNNER)) { |
| 22 | (Some(root), Some(rest)) => root.join(rest.trim_start_matches('/')), |
| 23 | _ => PathBuf::from(path), |
| 24 | } |
| 25 | } |
| 26 | |
| 27 | /// A path as the job's processes see it: with forward slashes, which every |
| 28 | /// shell takes, Windows' included. |
| 29 | pub(crate) fn shown(path: &std::path::Path) -> String { |
| 30 | let text = path.display().to_string(); |
| 31 | if cfg!(windows) { text.replace('\\', "/") } else { text } |
| 32 | } |
| 33 | |
| 34 | /// `text` with `/home/runner` moved to `root`. |
| 35 | fn moved(text: &str, root: &str) -> String { |
| 36 | match text.strip_prefix(HOME_RUNNER) { |
| 37 | Some(rest) if rest.is_empty() || rest.starts_with('/') => format!("{root}{rest}"), |
| 38 | _ => text.to_owned(), |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | /// Moves every `/home/runner` path in a spec's values (its variables, |
| 43 | /// `github` and `runner` contexts) to where this job keeps its files. |
| 44 | pub(crate) fn relocate(value: &mut Value) { |
| 45 | let Some(root) = root() else { return }; |
| 46 | relocate_to(value, &shown(&root)); |
| 47 | } |
| 48 | |
| 49 | fn relocate_to(value: &mut Value, root: &str) { |
| 50 | match value { |
| 51 | Value::String(text) => *text = moved(text, root), |
| 52 | Value::Array(items) => items.iter_mut().for_each(|item| relocate_to(item, root)), |
| 53 | Value::Object(fields) => fields.values_mut().for_each(|item| relocate_to(item, root)), |
| 54 | _ => {} |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | /// How `PATH` separates its entries here. |
| 59 | pub(crate) const PATH_SEPARATOR: &str = if cfg!(windows) { ";" } else { ":" }; |
| 60 | |
| 61 | #[cfg(test)] |
| 62 | mod tests { |
| 63 | use super::*; |
| 64 | use serde_json::json; |
| 65 | |
| 66 | #[test] |
| 67 | fn home_runner_paths_move_and_nothing_else_does() { |
| 68 | let mut spec = json!({ |
| 69 | "GITHUB_WORKSPACE": "/home/runner/work/repo", |
| 70 | "RUNNER_TEMP": "/home/runner/_temp", |
| 71 | "HOME": "/home/runnerish", |
| 72 | "list": ["/home/runner", "/etc/hosts"], |
| 73 | "n": 3, |
| 74 | }); |
| 75 | relocate_to(&mut spec, "/srv/jobs/job_1"); |
| 76 | assert_eq!( |
| 77 | spec, |
| 78 | json!({ |
| 79 | "GITHUB_WORKSPACE": "/srv/jobs/job_1/work/repo", |
| 80 | "RUNNER_TEMP": "/srv/jobs/job_1/_temp", |
| 81 | "HOME": "/home/runnerish", |
| 82 | "list": ["/srv/jobs/job_1", "/etc/hosts"], |
| 83 | "n": 3, |
| 84 | }) |
| 85 | ); |
| 86 | } |
| 87 | } |