mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-08 17:19:10 -10:00
horizon-prepare --launch-api: what this preparer supports, as JSON
The Launch API version (the same line as horizon-client's, a test reads it), the preparer's version, the pinned Minecraft and Java major, the resources' schema and output version that --check compares, the options a launcher may pass and the exit codes. A launcher asks it before it runs a preparer it didn't build. Part of #3545. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
ce8d86cb23
commit
89efa41590
5 files changed
+161
-10
No files matched your search
@@ -0,0 +1,113 @@
|
||||
//! The preparer's side of the Launch API (docs/api/launch.md): the options another launcher may
|
||||
//! pass and `horizon-prepare --launch-api`'s reply, so it can ask what this preparer supports
|
||||
//! before it runs one.
|
||||
|
||||
use crate::artifacts::Lock;
|
||||
use crate::pyjson::{self, Value};
|
||||
|
||||
/// The Launch API's version, the same line as `horizon-client --launch-api`'s
|
||||
/// (docs/api/stability.md): an addition makes the next patch.
|
||||
pub const VERSION: &str = "0.1.0";
|
||||
|
||||
/// What a launcher may pass, with its value's placeholder (none for a flag). The asset commands
|
||||
/// (`sounds`, `fonts`...) stay out: `--complete` runs what a launcher needs of them.
|
||||
pub const OPTIONS: &[(&str, Option<&str>)] = &[
|
||||
("--prepare", None),
|
||||
("--server-jar", Some("PATH")),
|
||||
("--minecraft-dir", Some("PATH")),
|
||||
("--version-json", Some("PATH")),
|
||||
("--client-jar", Some("PATH")),
|
||||
("--libraries-dir", Some("PATH")),
|
||||
("--assets-dir", Some("PATH")),
|
||||
("--java-home", Some("PATH")),
|
||||
("--root", Some("PATH")),
|
||||
("--output", Some("PATH")),
|
||||
("--check", None),
|
||||
("--complete", None),
|
||||
("--progress", Some("json")),
|
||||
("--launch-api", None),
|
||||
];
|
||||
|
||||
/// 0 done, 1 failed or gaps found, 2 a refused command line.
|
||||
pub const EXIT_CODES: &[i64] = &[0, 1, 2];
|
||||
|
||||
/// `--launch-api`'s reply for this preparer (`horizon`, its version) and the pins beside it.
|
||||
pub fn reply(horizon: &str, lock: &Lock) -> String {
|
||||
let int = |n: i64| Value::Int(n.to_string());
|
||||
let options = OPTIONS
|
||||
.iter()
|
||||
.map(|(name, value)| {
|
||||
Value::Object(vec![
|
||||
("name".into(), Value::str(*name)),
|
||||
("value".into(), value.map_or(Value::Null, Value::str)),
|
||||
])
|
||||
})
|
||||
.collect();
|
||||
let reply = Value::Object(vec![
|
||||
("launch_api".into(), Value::str(VERSION)),
|
||||
("horizon".into(), Value::str(horizon)),
|
||||
("minecraft".into(), Value::str(&lock.version)),
|
||||
("java_major".into(), int(lock.java_major)),
|
||||
("schema".into(), int(crate::SCHEMA)),
|
||||
(
|
||||
"output_version".into(),
|
||||
int(i64::from(crate::OUTPUT_VERSION)),
|
||||
),
|
||||
("options".into(), Value::Array(options)),
|
||||
(
|
||||
"exit_codes".into(),
|
||||
Value::Array(EXIT_CODES.iter().map(|&c| int(c)).collect()),
|
||||
),
|
||||
]);
|
||||
pyjson::dumps(&reply, &pyjson::INDENT2)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_reply_names_the_pins_and_the_layout() {
|
||||
let root = std::env::temp_dir().join(format!("horizon-launch-api-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
std::fs::write(
|
||||
root.join("research-lock.json"),
|
||||
r#"{"minecraft": {"version": "9.9", "java_major": 25}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let lock = Lock::read(&root).unwrap();
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
let reply = pyjson::loads(&reply("1.2.3", &lock)).unwrap();
|
||||
let text = |key: &str| reply.get(key).and_then(Value::as_str).map(str::to_owned);
|
||||
let int = |key: &str| match reply.get(key) {
|
||||
Some(Value::Int(n)) => n.clone(),
|
||||
other => panic!("{key}: {other:?}"),
|
||||
};
|
||||
assert_eq!(text("launch_api").as_deref(), Some(VERSION));
|
||||
assert_eq!(text("horizon").as_deref(), Some("1.2.3"));
|
||||
assert_eq!(text("minecraft").as_deref(), Some("9.9"));
|
||||
assert_eq!(int("java_major"), "25");
|
||||
assert_eq!(int("schema"), crate::SCHEMA.to_string());
|
||||
assert_eq!(int("output_version"), crate::OUTPUT_VERSION.to_string());
|
||||
let progress = reply
|
||||
.get("options")
|
||||
.and_then(|o| match o {
|
||||
Value::Array(items) => items
|
||||
.iter()
|
||||
.find(|i| i.get("name").and_then(Value::as_str) == Some("--progress")),
|
||||
_ => None,
|
||||
})
|
||||
.expect("--progress is listed");
|
||||
assert_eq!(progress.get("value").and_then(Value::as_str), Some("json"));
|
||||
}
|
||||
|
||||
/// One Launch API, one version: the client's and the preparer's move together.
|
||||
#[test]
|
||||
fn the_client_speaks_the_same_version() {
|
||||
let client = include_str!("../../horizon-client/src/game/launch_api.rs");
|
||||
assert!(
|
||||
client.contains(&format!("pub const VERSION: &str = \"{VERSION}\";")),
|
||||
"horizon-client's Launch API version is not {VERSION}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ pub mod exporters;
|
||||
mod files;
|
||||
mod inventory;
|
||||
pub mod java;
|
||||
pub mod launch_api;
|
||||
mod pool;
|
||||
pub mod pyjson;
|
||||
mod sounds;
|
||||
|
||||
@@ -19,6 +19,7 @@ Usage: horizon-prepare [--prepare] [--server-jar PATH] [--minecraft-dir PATH] [-
|
||||
[--version-json PATH] [--client-jar PATH] [--libraries-dir PATH] [--assets-dir PATH]
|
||||
[--java-home PATH] [--complete] [--progress json]
|
||||
horizon-prepare --check [--root PATH] [--output PATH]
|
||||
horizon-prepare --launch-api [--root PATH]
|
||||
horizon-prepare compile-exporters [--root PATH]
|
||||
horizon-prepare sounds [--minecraft-dir PATH] [--version-json PATH] [--assets-dir PATH] [--root PATH] [--output PATH]
|
||||
horizon-prepare panorama [--minecraft-dir PATH] [--version-json PATH] [--assets-dir PATH] [--root PATH] [--output PATH]
|
||||
@@ -52,6 +53,8 @@ Usage: horizon-prepare [--prepare] [--server-jar PATH] [--minecraft-dir PATH] [-
|
||||
for resources the game plays with nothing missing
|
||||
--progress json One JSON object a line on stdout: {\"message\": ...} for each log line,
|
||||
{\"fraction\": F} as the work advances (0 to 1), {\"error\": ...} on failure
|
||||
--launch-api Print what this preparer supports as JSON (the Launch API's version, the
|
||||
pinned Minecraft and Java, the resources' layout, the options) and exit 0
|
||||
|
||||
Needs Minecraft played once from the official launcher, and Java of the version
|
||||
research-lock.json pins. Releases use a runtime; development or changed exporters need a JDK.
|
||||
@@ -100,6 +103,7 @@ fn main() -> ExitCode {
|
||||
let mut check = false;
|
||||
let mut complete = false;
|
||||
let mut json = false;
|
||||
let mut launch_api = false;
|
||||
let mut args = std::env::args_os().skip(1).peekable();
|
||||
let assets = args.next_if(|a| {
|
||||
matches!(
|
||||
@@ -161,6 +165,10 @@ fn main() -> ExitCode {
|
||||
Ok(v) => Err(format!("--progress takes json, not {}", v.display())),
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
"--launch-api" if inline.is_none() => {
|
||||
launch_api = true;
|
||||
Ok(())
|
||||
}
|
||||
"-h" | "--help" => {
|
||||
print!("{HELP}");
|
||||
return ExitCode::SUCCESS;
|
||||
@@ -183,6 +191,18 @@ fn main() -> ExitCode {
|
||||
return ExitCode::from(2);
|
||||
};
|
||||
options.root = root;
|
||||
if launch_api {
|
||||
return match horizon_prepare::artifacts::Lock::read(&options.root) {
|
||||
Ok(lock) => {
|
||||
println!("{}", horizon_prepare::launch_api::reply(VERSION, &lock));
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("error: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
};
|
||||
}
|
||||
if check {
|
||||
if options.import || options.server_jar.is_some() {
|
||||
eprintln!("horizon-prepare: error: --check prepares nothing; leave out --prepare and --server-jar");
|
||||
@@ -330,4 +350,15 @@ mod tests {
|
||||
assert!(horizon_prepare::pyjson::loads(&line).is_ok(), "{line}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Every option `--launch-api` promises is one `--help` documents.
|
||||
#[test]
|
||||
fn launch_api_options_are_in_the_help() {
|
||||
for (name, _) in horizon_prepare::launch_api::OPTIONS {
|
||||
assert!(
|
||||
HELP.contains(&format!(" {name} ")),
|
||||
"{name} is not in --help"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-8
@@ -8,10 +8,10 @@ reference implementation. Proposed in horizon-rust/horizon#3545.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Version | 0.1.0, printed by `horizon-client --launch-api` |
|
||||
| Version | 0.1.0, printed by `horizon-client --launch-api` and `horizon-prepare --launch-api` |
|
||||
| Level | experimental ([stability.md](stability.md)): preview once preparing and the guide are covered too |
|
||||
| Covers | starting `horizon-client`: its options, the session JSON, the exit codes, the crash report's path; preparing with `horizon-prepare`: `--output`, `--check`, `--complete`, `--progress json` |
|
||||
| Not yet | finding a build, asking the preparer what it supports (see Not there yet) |
|
||||
| Covers | starting `horizon-client`: its options, the session JSON, the exit codes, the crash report's path; preparing with `horizon-prepare`: `--launch-api`, `--output`, `--check`, `--complete`, `--progress json` |
|
||||
| Not yet | finding a build (see Not there yet) |
|
||||
|
||||
## Guide
|
||||
|
||||
@@ -58,10 +58,18 @@ downloads nothing: it reads the files of a Minecraft the player owns (Guide, ste
|
||||
pins in `research-lock.json`.
|
||||
|
||||
```sh
|
||||
horizon-prepare --launch-api
|
||||
horizon-prepare --prepare --output PREPARED --complete --progress json
|
||||
horizon-prepare --check --output PREPARED
|
||||
```
|
||||
|
||||
- `--launch-api`: prints one JSON object on stdout and exits 0 (1, with an `error:` line, when there is no
|
||||
`research-lock.json` beside it or at `--root`). `launch_api` is the version, the same as
|
||||
`horizon-client --launch-api`'s; `horizon` is the preparer's own; `minecraft` and `java_major` are the pins it
|
||||
prepares for (the game files it needs, and the Java `--java-home` must give); `schema` and `output_version`
|
||||
are the layout and contents of the resources it writes, the ones `--check` compares; `options` lists the
|
||||
options above and below as `{"name", "value"}` (`value` is the placeholder, null for a flag) and
|
||||
`exit_codes` its exit codes. Pass only options it lists; readers ignore fields they don't know.
|
||||
- `--output DIR`: where the resources go; the preparer's scratch files stay in its own `.local/`. One
|
||||
preparation at a time writes to a folder; a second one waits.
|
||||
- `--check`: exit 0 when the resources are complete and current for this preparer. Otherwise exit 1, with each
|
||||
@@ -105,14 +113,12 @@ test keeps the contract and the code together.
|
||||
|
||||
## Not there yet
|
||||
|
||||
- **Asking the preparer what it supports:** `horizon-prepare --launch-api`, with its output version, schema and
|
||||
Java major.
|
||||
- **Finding a build:** a signed release index and builds kept longer than seven days.
|
||||
- **A guide**, "Running Horizon from another launcher", from picking a build to playing.
|
||||
|
||||
## Changelog
|
||||
|
||||
- 0.1.0 (not released yet, so still one version): `horizon-client`'s `--game-dir`, `--home`, `--player-dir`, `--fullscreen`,
|
||||
`--session-stdin`, `--launch-api`, exit code 2 for a refused command line; `horizon-prepare`'s `--output`,
|
||||
`--check`, `--complete`, `--progress json`, `--version-json`, `--client-jar`, `--libraries-dir`, `--assets-dir`
|
||||
and `--java-home`.
|
||||
`--session-stdin`, `--launch-api`, exit code 2 for a refused command line; `horizon-prepare`'s `--launch-api`,
|
||||
`--output`, `--check`, `--complete`, `--progress json`, `--version-json`, `--client-jar`, `--libraries-dir`,
|
||||
`--assets-dir` and `--java-home`.
|
||||
@@ -72,7 +72,7 @@ Every preview and stable API has tests that run old users against the current co
|
||||
| Agent control | a transcript per protocol version: commands and the reply keys each must have |
|
||||
| Server | `api::ROUTES` against `route()` (generated reference); a `server.toml` from each release line parses to the same settings; golden replies per endpoint |
|
||||
| Launcher | the generated reference; golden `--json` output per command |
|
||||
| Launch | the generated reference, which holds `--launch-api`'s reply; every listed option is accepted; `horizon-prepare`'s `--progress json` lines (`progress_json_lines_keep_their_shape`) |
|
||||
| Launch | the generated reference, which holds `--launch-api`'s reply; every listed option is accepted; `horizon-prepare`'s `--progress json` lines (`progress_json_lines_keep_their_shape`), its `--launch-api` reply and options (`launch_api::tests`, `launch_api_options_are_in_the_help`) |
|
||||
| Data formats | a sample file of every version a reader accepts, read by today's reader |
|
||||
|
||||
The generated references are the API's own diff: a pull request that changes an API changes its reference, and the
|
||||
@@ -96,7 +96,7 @@ Deprecations and breaking changes are in bold. The release notes of the game rel
|
||||
| `horizon-server` commands, `server.toml` | stable | `horizon-server --version` |
|
||||
| Server control API | stable | `/api/v1` |
|
||||
| `horizon-launcher` commands and MCP tools | preview | none yet (unversioned) |
|
||||
| Launch API: `horizon-client`'s launch options, session JSON, exit codes; `horizon-prepare`'s output and progress | experimental | 0.1.0, `horizon-client --launch-api` |
|
||||
| Launch API: `horizon-client`'s launch options, session JSON, exit codes; `horizon-prepare`'s output and progress | experimental | 0.1.0, `horizon-client --launch-api` and `horizon-prepare --launch-api` |
|
||||
| Benchmark report | stable | `"version": 1` |
|
||||
| Telemetry batch and consent | stable | `"schema": 1`, consent `2` |
|
||||
| `horizon-graphics.txt`, `horizon-extras.txt`, `horizon-mods.txt` | preview | none |
|
||||
|
||||
Reference in new issue
Block a user