sergio/takana
> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
takana etapa 4: los 10 crates de librería y el CLI pasan a takana-*
1//! Integration test del path autotools:
2//! - Repo git con `configure.ac` + `Makefile.am` + `hello.c` (sin `configure` generado).
3//! - Receta SIN `[build.phases]` → la heurística debe detectar AutoconfRaw y emitir
4//! `autoreconf -fi && ./configure --prefix=/usr` + `make` + `make install DESTDIR=/out`.
5//! - El binario instalado en el store debe correr en el sandbox.
6//!
7//! Skip transparente si el rootfs Alpine o zig no están bootstrappeados.
8
9use std::path::{Path, PathBuf};
10use std::process::Command;
11
12use takana_build::{build, BuildConfig};
13use takana_core::{Recipe, Store};
14
15const HELLO_C: &str = r#"
16#include <stdio.h>
17int main(void) { puts("hammer-autotools OK"); return 0; }
18"#;
19
20const CONFIGURE_AC: &str = r#"
21AC_INIT([helloat], [1.0])
22AM_INIT_AUTOMAKE([foreign -Wall])
23AC_PROG_CC
24AC_CONFIG_FILES([Makefile])
25AC_OUTPUT
26"#;
27
28const MAKEFILE_AM: &str = r#"
29bin_PROGRAMS = helloat
30helloat_SOURCES = hello.c
31"#;
32
33const RECIPE_TOML: &str = r#"
34name = "helloat"
35version = "1.0"
36
37[source]
38repo = "PLACEHOLDER_REPO_URL"
39commit = "PLACEHOLDER_COMMIT"
40
41[build]
42compiler = "zig-cc"
43target = "x86_64-linux-musl"
44link = "static"
45"#;
46
47fn project_root() -> PathBuf {
48 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
49 .parent()
50 .unwrap()
51 .parent()
52 .unwrap()
53 .to_path_buf()
54}
55
56fn skip_if_no_layout(cfg: &BuildConfig) -> bool {
57 if !cfg.rootfs.join("bin/busybox").is_file() {
58 eprintln!("SKIP: rootfs Alpine no encontrado en {}.", cfg.rootfs.display());
59 return true;
60 }
61 // Las tools de autotools se metieron con `apk add make autoconf automake m4 ...`
62 // — comprobamos uno para no engañarnos si el rootfs es el minirootfs pelado.
63 if !cfg.rootfs.join("usr/bin/autoreconf").is_file() {
64 eprintln!(
65 "SKIP: rootfs sin autotools. Corre: bwrap --bind {} / --share-net /sbin/apk add make autoconf automake m4 patch coreutils libtool pkgconf bash",
66 cfg.rootfs.display()
67 );
68 return true;
69 }
70 if !cfg.zig_dir.join("zig").is_file() {
71 eprintln!("SKIP: zig no encontrado en {}.", cfg.zig_dir.display());
72 return true;
73 }
74 false
75}
76
77fn git(args: &[&str], cwd: &Path) {
78 let st = Command::new("git").args(args).current_dir(cwd).status().expect("git");
79 assert!(st.success(), "git {args:?} falló");
80}
81
82fn init_repo(upstream: &Path) -> String {
83 std::fs::create_dir_all(upstream).unwrap();
84 std::fs::write(upstream.join("configure.ac"), CONFIGURE_AC).unwrap();
85 std::fs::write(upstream.join("Makefile.am"), MAKEFILE_AM).unwrap();
86 std::fs::write(upstream.join("hello.c"), HELLO_C).unwrap();
87 git(&["init", "-q", "-b", "main"], upstream);
88 git(&["config", "user.email", "test@hammer"], upstream);
89 git(&["config", "user.name", "hammer-test"], upstream);
90 git(&["add", "."], upstream);
91 git(&["commit", "-q", "-m", "init"], upstream);
92 let out = Command::new("git")
93 .args(["rev-parse", "HEAD"])
94 .current_dir(upstream)
95 .output()
96 .unwrap();
97 String::from_utf8(out.stdout).unwrap().trim().to_string()
98}
99
100#[test]
101fn build_autotools_hello_end_to_end() {
102 let _ = tracing_subscriber::fmt().with_test_writer().try_init();
103
104 let project = project_root();
105 let cfg = BuildConfig {
106 rootfs: project.join(".dev-fs/alpine"),
107 zig_dir: project.join(".dev-fs/tools/zig"),
108 work_root: tempfile::tempdir().unwrap().keep(),
109 cache_root: Some(project.join(".dev-fs/cache")),
110 };
111 if skip_if_no_layout(&cfg) {
112 return;
113 }
114
115 let tmp = tempfile::tempdir().unwrap();
116 let store_root = tmp.path().join("store");
117 std::fs::create_dir_all(&store_root).unwrap();
118
119 let upstream = tmp.path().join("upstream");
120 let commit = init_repo(&upstream);
121 let repo_url = format!("file://{}", upstream.display());
122
123 let recipe_dir = tmp.path().join("recipe");
124 std::fs::create_dir_all(&recipe_dir).unwrap();
125 let toml = RECIPE_TOML
126 .replace("PLACEHOLDER_REPO_URL", &repo_url)
127 .replace("PLACEHOLDER_COMMIT", &commit);
128 let recipe_path = recipe_dir.join("helloat.toml");
129 std::fs::write(&recipe_path, toml).unwrap();
130 let recipe = Recipe::load_from_path(&recipe_path).expect("load recipe");
131
132 let store = Store::open(&store_root).unwrap();
133 let h = build(&recipe, &cfg, &store).expect("build helloat");
134 let sealed = store.path_of(&h, &recipe.name);
135
136 let bin = sealed.join("usr/bin/helloat");
137 assert!(bin.is_file(), "esperaba {}", bin.display());
138
139 let header = std::fs::read(&bin).unwrap();
140 assert_eq!(&header[..4], b"\x7fELF", "es ELF");
141
142 let out = Command::new("bwrap")
143 .args([
144 "--overlay-src",
145 cfg.rootfs.to_str().unwrap(),
146 "--tmp-overlay",
147 "/",
148 "--proc",
149 "/proc",
150 "--dev",
151 "/dev",
152 "--ro-bind",
153 sealed.to_str().unwrap(),
154 "/art",
155 "--unshare-all",
156 "/art/usr/bin/helloat",
157 ])
158 .output()
159 .expect("spawn bwrap");
160 assert!(out.status.success(), "helloat falló: {:?}", out);
161 assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "hammer-autotools OK");
162}