Skip to content

Commit 39e50e2

Browse files
authored
Merge pull request #94 from httpsphl/ci/unix-runners
ci: the daemon on Ubuntu and macOS
2 parents 204dd6a + 5634014 commit 39e50e2

8 files changed

Lines changed: 75 additions & 23 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,28 @@ jobs:
3131
- name: Generated TypeScript bindings are up to date
3232
run: git diff --exit-code -- app/src/lib/protocol.gen.ts
3333

34-
rust-ubuntu:
35-
name: Rust core + store (Ubuntu)
36-
runs-on: ubuntu-latest
34+
# The daemon and its crates off Windows (spec 18, item 8). Plain `cargo`
35+
# builds the default members only: the app crates need the desktop
36+
# libraries of each system and join later.
37+
rust-unix:
38+
name: Rust (${{ matrix.name }})
39+
strategy:
40+
fail-fast: false
41+
matrix:
42+
include:
43+
- name: Ubuntu
44+
os: ubuntu-latest
45+
- name: macOS
46+
os: macos-latest
47+
runs-on: ${{ matrix.os }}
3748
steps:
3849
- uses: actions/checkout@v5
3950
- uses: dtolnay/rust-toolchain@stable
4051
with:
4152
components: clippy
4253
- uses: Swatinem/rust-cache@v2
43-
- run: cargo clippy -p botloft-core -p botloft-store --all-targets -- -D warnings
44-
- run: cargo test -p botloft-core -p botloft-store
54+
- run: cargo clippy --all-targets -- -D warnings
55+
- run: cargo test
4556

4657
app:
4758
name: App (Windows)

‎crates/botloftd/build.rs‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,18 @@ fn main() {
3636
let out =
3737
PathBuf::from(env::var_os("OUT_DIR").expect("cargo sets OUT_DIR")).join("botloftd.rc");
3838
fs::write(&out, script).expect("cannot write the resource script");
39-
if let Err(err) = embed_resource::compile(&out, embed_resource::NONE).manifest_required() {
39+
embed(&out);
40+
}
41+
42+
/// `embed-resource` is a build dependency on Windows hosts only.
43+
#[cfg(windows)]
44+
fn embed(script: &std::path::Path) {
45+
if let Err(err) = embed_resource::compile(script, embed_resource::NONE).manifest_required() {
4046
panic!("cannot embed the resources: {err}");
4147
}
4248
}
49+
50+
#[cfg(not(windows))]
51+
fn embed(_script: &std::path::Path) {
52+
panic!("the Windows daemon is built on Windows, where its resources can be embedded");
53+
}

‎crates/botloftd/src/autostart/binary.rs‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ mod tests {
109109
names
110110
}
111111

112+
/// The old copy and the installed binary, in the order `names` gives.
113+
fn old_and_installed() -> Vec<String> {
114+
let mut names = vec!["botloftd.1.old".to_owned(), EXE_NAME.to_owned()];
115+
names.sort();
116+
names
117+
}
118+
112119
#[test]
113120
fn copies_once_and_replaces_only_a_different_binary() {
114121
let dir = tempfile::tempdir().expect("tempdir");
@@ -129,13 +136,13 @@ mod tests {
129136
let second = install(&source, &bin).expect("update");
130137
assert!(second.changed);
131138
assert_eq!(fs::read(&second.path).expect("read"), b"v2");
132-
assert_eq!(names(&bin), ["botloftd.1.old".to_owned(), EXE_NAME.into()]);
139+
assert_eq!(names(&bin), old_and_installed());
133140

134141
fs::write(&source, b"v3").expect("write");
135142
install(&source, &bin).expect("update");
136143
assert_eq!(
137144
names(&bin),
138-
["botloftd.1.old".to_owned(), EXE_NAME.into()],
145+
old_and_installed(),
139146
"the v1 copy went away and v2 moved aside"
140147
);
141148

‎crates/botloftd/src/browser/sites.rs‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,17 @@ pub fn file_allowed(url: &str, folders: &[PathBuf]) -> bool {
126126
is_file(url) && inside(&file_path(url), folders).is_some()
127127
}
128128

129-
/// The path of a `file:` URL: `file:///C:/a%20b/x.html?q` is `C:/a b/x.html`.
129+
/// The path of a `file:` URL: `file:///C:/a%20b/x.html?q` is `C:/a b/x.html`
130+
/// on Windows, and `file:///home/ana/x.html` is `/home/ana/x.html` elsewhere.
130131
fn file_path(url: &str) -> PathBuf {
131132
let rest = url.get(5..).unwrap_or_default();
132133
let rest = rest.split(['?', '#']).next().unwrap_or_default();
133-
PathBuf::from(decode(rest.trim_start_matches('/')))
134+
let path = decode(rest.trim_start_matches('/'));
135+
if cfg!(windows) {
136+
PathBuf::from(path)
137+
} else {
138+
PathBuf::from(format!("/{path}"))
139+
}
134140
}
135141

136142
fn file(path: &Path, base: &Path, folders: &[PathBuf]) -> Result<Place, String> {
@@ -164,13 +170,17 @@ fn inside(path: &Path, folders: &[PathBuf]) -> Option<PathBuf> {
164170
.then_some(real)
165171
}
166172

167-
/// `C:\a b\x.html` as `file:///C:/a%20b/x.html`.
173+
/// `C:\a b\x.html` as `file:///C:/a%20b/x.html`, and `/home/ana/x.html` as
174+
/// `file:///home/ana/x.html`.
168175
fn file_url(path: &Path) -> String {
169176
let text = path.to_string_lossy();
170-
let text = text
171-
.strip_prefix(r"\\?\")
172-
.unwrap_or(&text)
173-
.replace('\\', "/");
177+
let text = if cfg!(windows) {
178+
text.strip_prefix(r"\\?\")
179+
.unwrap_or(&text)
180+
.replace('\\', "/")
181+
} else {
182+
text.trim_start_matches('/').to_owned()
183+
};
174184
let mut url = String::from("file:///");
175185
for byte in text.bytes() {
176186
match byte {

‎crates/botloftd/src/config.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ mod tests {
218218
#[test]
219219
fn keys_override_defaults() {
220220
let config = Config::parse(
221-
"port = 45999\nworkspaces_root = 'D:\\\\bots'\n[courier]\nmax_attempts = 3\n",
221+
"port = 45999\nworkspaces_root = 'D:\\bots'\n[courier]\nmax_attempts = 3\n",
222222
)
223223
.expect("parse");
224224
assert_eq!(config.port, 45999);

‎crates/botloftd/src/platform/unix.rs‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,17 @@ pub fn user_environment() -> io::Result<Vec<(OsString, OsString)>> {
6868
.collect())
6969
}
7070

71-
/// The login name; Unix has no display name to ask for.
71+
/// The login name; Unix has no display name to ask for. `USER` is missing
72+
/// in some service and container environments, and the home folder is
73+
/// named after the login there too.
7274
pub fn owner_name() -> String {
73-
std::env::var("USER").unwrap_or_default()
75+
["USER", "LOGNAME"]
76+
.iter()
77+
.filter_map(std::env::var_os)
78+
.chain(dirs::home_dir().and_then(|home| home.file_name().map(ToOwned::to_owned)))
79+
.map(|name| name.to_string_lossy().trim().to_owned())
80+
.find(|name| !name.is_empty())
81+
.unwrap_or_default()
7482
}
7583

7684
/// Nothing to leave: Unix never opens a console for the daemon.
@@ -87,8 +95,9 @@ pub fn register_task(_name: &str, _task: &TaskDefinition) -> io::Result<()> {
8795
Err(no_tasks())
8896
}
8997

98+
/// Nothing can be installed here yet, so there is never a task to find.
9099
pub fn find_task(_name: &str) -> io::Result<Option<TaskInfo>> {
91-
Err(no_tasks())
100+
Ok(None)
92101
}
93102

94103
pub fn run_task(_name: &str) -> io::Result<()> {
@@ -100,7 +109,7 @@ pub fn stop_task(_name: &str) -> io::Result<()> {
100109
}
101110

102111
pub fn delete_task(_name: &str) -> io::Result<bool> {
103-
Err(no_tasks())
112+
Ok(false)
104113
}
105114

106115
/// There is no Recycle Bin off Windows: the folder stays.

‎crates/botloftd/tests/courier.rs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,15 +188,19 @@ async fn a_message_the_bot_began_is_not_sent_again() {
188188
assert!(second.input_lines().is_empty());
189189
}
190190

191-
#[tokio::test]
191+
// Two workers: the held spawn blocks the supervisor's.
192+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
192193
async fn archiving_a_bot_drops_what_is_still_queued_for_it() {
193194
let t = TestDaemon::start_supervised().await;
195+
// The bot is still starting, so nothing can take the message yet.
196+
let (_began, release) = t.runtime.hold_next_spawn();
194197
let mut app = t.session().await;
195198
let bot = crew_and_bot(&mut app).await;
196199
send(&mut app, &bot, "never read").await;
197200
app.call("bots.archive", json!({ "botId": bot["id"] }))
198201
.await
199202
.expect("archive");
203+
release.send(()).expect("release");
200204
t.clock.advance(Duration::from_secs(5));
201205
let dead = delivery(&mut app, "dead").await;
202206
assert_eq!(dead["lastError"], "the bot was archived");

‎docs/spec.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -925,7 +925,7 @@ Esta seção entra no PR do I1, junto com o código.
925925
- TS: `tsc --noEmit`, `biome check`, `vitest run`.
926926
- Limite **flexível de 300 linhas** por arquivo; passou disso, dividir por responsabilidade.
927927
- Toda lógica de supervisor, courier e chat testada com `FakeRuntime` (sem Claude real). O `FakeRuntime` fala `stream-json` de verdade: recebe as linhas do stdin e o teste escreve os eventos do stdout.
928-
- CI: GitHub Actions em `windows-latest` (principal) e `ubuntu-latest` para `botloft-core` e `botloft-store`.
928+
- CI: GitHub Actions em `windows-latest` (principal, workspace inteiro e app) e em `ubuntu-latest` e `macos-latest` para os membros padrão (`botloft-core`, `botloft-store`, `botloftd`), com clippy e testes. Os crates do app entram nesses runners quando o app rodar neles (18, item 8).
929929

930930
## 17. Marcos do MVP
931931

@@ -950,7 +950,7 @@ M2 a M4 foram entregues com ConPTY, terminal com replay, inbox por named pipe e
950950
5. Histórico de versões das instruções do bot.
951951
6. Acesso remoto com token por dispositivo (Tailscale).
952952
7. Sinais entre bots disparando rotinas.
953-
8. Suporte Linux/macOS.
953+
8. Suporte Linux/macOS. Em fatias: (1) CI com Ubuntu e macOS para o daemon; (2) runtime do bot (grupo de processos, ambiente, `claude`, navegador); (3) iniciar com o sistema (`systemd --user`, LaunchAgent); (4) app; (5) empacotamento e release. Feita: 1.
954954
9. Navegador dos bots, com o dono assistindo ao vivo. Desenho na seção 21.
955955

956956
## 19. Pontos a verificar na versão alvo do Claude Code

0 commit comments

Comments
 (0)