1use std::fs;
9use std::path::{Path, PathBuf};
10
11use anyhow::{Context, Result};
12use include_dir::{include_dir, Dir};
13
14static ASSETS: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/assets");
15
16fn cache_dir() -> Result<PathBuf> {
19 if let Some(dir) = std::env::var_os("RUN_ASSET_DIR") {
20 return Ok(PathBuf::from(dir));
21 }
22 let base = std::env::var_os("XDG_CACHE_HOME")
23 .map(PathBuf::from)
24 .or_else(|| dirs_home().map(|home| home.join("Library/Caches")))
25 .context("no cache directory — set RUN_ASSET_DIR")?;
26 Ok(base.join("run-stack").join(env!("CARGO_PKG_VERSION")))
27}
28
29fn dirs_home() -> Option<PathBuf> {
30 std::env::var_os("HOME").map(PathBuf::from)
31}
32
33pub fn package_dir() -> Result<PathBuf> {
38 if let Some(dir) = std::env::var("RUN_PACKAGE_DIR").ok().filter(|d| !d.is_empty()) {
39 return Ok(PathBuf::from(dir));
40 }
41 let dir = cache_dir()?;
42 let stamp = dir.join(".unpacked");
43 if !stamp.is_file() {
44 unpack(&dir)?;
45 fs::write(&stamp, env!("CARGO_PKG_VERSION"))
46 .with_context(|| format!("writing {}", stamp.display()))?;
47 }
48 Ok(dir)
49}
50
51fn unpack(dir: &Path) -> Result<()> {
52 fs::create_dir_all(dir).with_context(|| format!("creating {}", dir.display()))?;
53 ASSETS
54 .extract(dir)
55 .with_context(|| format!("unpacking the stack files into {}", dir.display()))?;
56
57 #[cfg(unix)]
60 make_scripts_executable(&ASSETS, dir)?;
61 Ok(())
62}
63
64#[cfg(unix)]
65fn make_scripts_executable(source: &Dir<'_>, target: &Path) -> Result<()> {
66 use std::os::unix::fs::PermissionsExt;
67 for file in source.files() {
68 let executable = file
69 .path()
70 .extension()
71 .is_some_and(|extension| extension == "sh" || extension == "py");
72 if !executable {
73 continue;
74 }
75 let path = target.join(file.path());
76 let mut permissions = fs::metadata(&path)
77 .with_context(|| format!("reading {}", path.display()))?
78 .permissions();
79 permissions.set_mode(0o755);
80 fs::set_permissions(&path, permissions)
81 .with_context(|| format!("making {} executable", path.display()))?;
82 }
83 for nested in source.dirs() {
84 make_scripts_executable(nested, target)?;
85 }
86 Ok(())
87}
88
89pub fn read(path: &str) -> Option<&'static str> {
91 ASSETS.get_file(path).and_then(|file| file.contents_utf8())
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn carries_what_a_stack_is_built_from() {
100 assert!(read("docker-compose.yml").is_some_and(|f| f.contains("x-frontend-service")));
101 assert!(read(".env.example").is_some());
102 assert!(ASSETS.get_file("docker/frontend/entrypoint.sh").is_some());
103 assert!(ASSETS.get_file("docker/backend/laravel/Dockerfile").is_some());
104 assert!(ASSETS.get_file("docker/dashboard/server.py").is_some());
105 }
106
107 #[test]
108 fn unpacks_a_usable_tree() {
109 let dir = tempfile::tempdir().unwrap();
110 unpack(dir.path()).unwrap();
111 assert!(dir.path().join("docker-compose.yml").is_file());
112 #[cfg(unix)]
113 {
114 use std::os::unix::fs::PermissionsExt;
115 let entrypoint = dir.path().join("docker/frontend/entrypoint.sh");
116 let mode = fs::metadata(&entrypoint).unwrap().permissions().mode();
117 assert_eq!(mode & 0o111, 0o111, "entrypoints must stay executable");
118 }
119 }
120
121 #[test]
124 #[test]
125 fn the_shell_scripts_pick_an_interpreter_that_can_read_toml() {
126 let common = read("scripts/common.sh").expect("common.sh ships");
130
131 assert!(common.contains("run_python()"), "no interpreter resolver");
132 assert!(common.contains("import tomllib"), "resolver does not test for tomllib");
133
134 for name in ["scripts/common.sh", "scripts/init.sh", "scripts/ports.sh"] {
135 let text = read(name).unwrap_or_default();
136 assert!(
137 !text.contains("\npython3 ") && !text.contains("$(python3 "),
138 "{name} still calls a bare python3"
139 );
140 }
141 }
142
143 fn matches_the_shell_version() {
144 let shell = Path::new(env!("CARGO_MANIFEST_DIR")).join("../run");
145 if !shell.join("docker-compose.yml").is_file() {
146 return;
147 }
148 for name in ["docker-compose.yml", ".env.example"] {
149 let theirs = fs::read_to_string(shell.join(name)).unwrap();
150 assert_eq!(
151 read(name).unwrap(),
152 theirs,
153 "{name} has drifted from ../run — copy it across"
154 );
155 }
156 }
157}