Include voice runtime in Windows portable ZIP packages

This commit is contained in:
Benjamin Carlsson
2026-09-11 03:50:05 -07:00
parent 5b8ebe76ab
commit cd47194867
5 changed files with 151 additions and 18 deletions
+5 -18
View File
@@ -604,25 +604,12 @@ jobs:
# Must run from inside the dest dir so 7z does not embed the
# directory path inside the zip.
if [[ "$base" == "codex-${target}.exe" ]]; then
# Bundle the sandbox helper binaries into the main codex zip so
# WinGet installs include the required helpers next to codex.exe.
# Fall back to the single-binary zip if the helpers are missing
# to avoid breaking releases.
# Preserve WinGet executable paths while including the signed
# runtime, resources, and package metadata from the canonical tar.
bundle_dir="$(mktemp -d)"
runner_src="$dest/codex-command-runner-${target}.exe"
setup_src="$dest/codex-windows-sandbox-setup-${target}.exe"
if [[ -f "$runner_src" && -f "$setup_src" ]]; then
cp "$dest/$base" "$bundle_dir/$base"
cp "$runner_src" "$bundle_dir/codex-command-runner.exe"
cp "$setup_src" "$bundle_dir/codex-windows-sandbox-setup.exe"
# Use an absolute path so bundle zips land in the real dist
# dir even when 7z runs from a temp directory.
(cd "$bundle_dir" && 7z a "$repo_root/$dest/${base}.zip" .)
else
echo "warning: missing sandbox binaries; falling back to single-binary zip"
echo "warning: expected $runner_src and $setup_src"
(cd "$dest" && 7z a "${base}.zip" "$base")
fi
tar -xzf "$dest/codex-package-${target}.tar.gz" -C "$bundle_dir"
python "${GITHUB_WORKSPACE}/scripts/build_winget_package.py" "$(cygpath -w "$bundle_dir")"
(cd "$bundle_dir" && 7z a "$repo_root/$dest/${base}.zip" .)
rm -rf "$bundle_dir"
else
(cd "$dest" && 7z a "${base}.zip" "$base")
@@ -46,3 +46,31 @@ fn bundle_executable_preserves_package_layout_and_install_method() -> std::io::R
);
Ok(())
}
#[cfg(windows)]
#[test]
fn winget_root_requires_metadata_for_the_actual_executable() -> std::io::Result<()> {
let temp = tempfile::tempdir()?;
let package = canonical_absolute_path(temp.path()).unwrap();
let name = "codex-x86_64-pc-windows-msvc.exe";
let executable = package.join(name);
fs::write(&executable, "signed CLI")?;
for directory in [RESOURCES_DIRNAME, PATH_DIRNAME] {
fs::create_dir_all(package.join(directory))?;
}
assert_eq!(CodexPackageLayout::from_exe(executable.as_path()), None);
for entrypoint in ["other.exe", "bin/codex.exe", name] {
fs::write(
package.join(PACKAGE_METADATA_FILENAME),
serde_json::json!({"layoutVersion": 1, "entrypoint": entrypoint}).to_string(),
)?;
let expected = (entrypoint == name).then(|| CodexPackageLayout {
package_dir: package.clone(),
bin_dir: package.clone(),
resources_dir: Some(package.join(RESOURCES_DIRNAME)),
path_dir: Some(package.join(PATH_DIRNAME)),
});
assert_eq!(CodexPackageLayout::from_exe(executable.as_path()), expected);
}
Ok(())
}
+15
View File
@@ -246,6 +246,21 @@ impl CodexPackageLayout {
fn from_exe(exe_path: &Path) -> Option<Self> {
let canonical_exe = canonical_absolute_path(exe_path)?;
let exe_dir = canonical_exe.parent()?;
// WinGet preserves a target-qualified executable at the package root.
// Only recognize that layout when metadata names this exact executable.
#[cfg(windows)]
if let Ok(contents) = std::fs::read(exe_dir.join(PACKAGE_METADATA_FILENAME))
&& let Ok(metadata) = serde_json::from_slice::<serde_json::Value>(&contents)
&& metadata["layoutVersion"] == 1
&& metadata["entrypoint"].as_str().map(OsStr::new) == canonical_exe.file_name()
{
return Some(Self {
resources_dir: existing_dir(exe_dir.join(RESOURCES_DIRNAME)),
path_dir: existing_dir(exe_dir.join(PATH_DIRNAME)),
package_dir: exe_dir.clone(),
bin_dir: exe_dir,
});
}
match exe_dir.file_name() {
Some(name) if name == OsStr::new(BIN_DIRNAME) => Self::from_package_bin_dir(exe_dir),
Some(name) if name == OsStr::new(RESOURCES_DIRNAME) => {
+37
View File
@@ -0,0 +1,37 @@
"""Adapt an extracted signed Windows package to WinGet's existing root filenames."""
import argparse
import json
import shutil
from pathlib import Path
def prepare_winget_package(package: Path) -> None:
metadata_path = package / "codex-package.json"
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
target = metadata["target"]
if target not in ("x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"):
raise ValueError("WinGet requires a Windows package")
if metadata["entrypoint"] != "bin/codex.exe":
raise ValueError("WinGet requires the canonical Codex entrypoint")
entrypoint = f"codex-{target}.exe"
manifest_path = package / "codex-resources/voice/manifest.json"
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
manifest["sha256"][entrypoint] = manifest["sha256"].pop("bin/codex.exe")
(package / "bin/codex.exe").rename(package / entrypoint)
(package / "bin/codex-code-mode-host.exe").rename(
package / "codex-code-mode-host.exe"
)
# Keep resources in place for package-aware discovery and provide the root
# filenames declared by the existing WinGet portable installer manifest.
for helper in ("codex-command-runner.exe", "codex-windows-sandbox-setup.exe"):
shutil.copy2(package / "codex-resources" / helper, package / helper)
metadata["entrypoint"] = entrypoint
for path, value in ((metadata_path, metadata), (manifest_path, manifest)):
path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("package", type=Path)
prepare_winget_package(parser.parse_args().package)
+66
View File
@@ -1,12 +1,15 @@
#!/usr/bin/env python3
from pathlib import Path
import hashlib
import json
import sys
import tempfile
import unittest
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from build_winget_package import prepare_winget_package
from codex_package.layout import build_package_dir
from codex_package.layout import validate_package_dir
from codex_package.targets import PACKAGE_VARIANTS
@@ -15,6 +18,69 @@ from codex_package.targets import TARGET_SPECS
class PackageLayoutTest(unittest.TestCase):
def test_winget_preserves_signed_files_and_voice_hashes(self) -> None:
for target in ("x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"):
with self.subTest(target=target), tempfile.TemporaryDirectory() as temp:
package = Path(temp)
files = {
"bin/codex.exe": b"signed CLI",
"bin/codex-code-mode-host.exe": b"signed code mode host",
"codex-resources/codex-command-runner.exe": b"signed runner",
"codex-resources/codex-windows-sandbox-setup.exe": b"signed setup",
"codex-resources/voice/bin/codex-voice-host.exe": b"signed voice host",
"codex-resources/voice/bin/gstreamer-1.0-0.dll": b"signed audio DLL",
"codex-resources/voice/NOTICE.md": b"license notices",
"codex-path/rg.exe": b"ripgrep",
}
for name, contents in files.items():
path = package / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(contents)
metadata = {
"layoutVersion": 1,
"target": target,
"entrypoint": "bin/codex.exe",
}
(package / "codex-package.json").write_text(json.dumps(metadata))
manifest = {
"schemaVersion": 1,
"sha256": {
name: hashlib.sha256(contents).hexdigest()
for name, contents in files.items()
if name == "bin/codex.exe"
or name.startswith("codex-resources/voice/")
},
}
manifest_path = package / "codex-resources/voice/manifest.json"
manifest_path.write_text(json.dumps(manifest))
prepare_winget_package(package)
entrypoint = f"codex-{target}.exe"
files[entrypoint] = files.pop("bin/codex.exe")
files["codex-code-mode-host.exe"] = files.pop(
"bin/codex-code-mode-host.exe"
)
for helper in (
"codex-command-runner.exe",
"codex-windows-sandbox-setup.exe",
):
files[helper] = files[f"codex-resources/{helper}"]
actual = {
str(path.relative_to(package)).replace("\\", "/"): path.read_bytes()
for path in package.rglob("*")
if path.is_file()
}
actual_metadata = json.loads(actual.pop("codex-package.json"))
actual_manifest = json.loads(
actual.pop("codex-resources/voice/manifest.json")
)
self.assertEqual(actual, files)
metadata["entrypoint"] = entrypoint
self.assertEqual(actual_metadata, metadata)
manifest["sha256"][entrypoint] = manifest["sha256"].pop("bin/codex.exe")
self.assertEqual(actual_manifest, manifest)
for name, digest in actual_manifest["sha256"].items():
self.assertEqual(hashlib.sha256(actual[name]).hexdigest(), digest)
def test_macos_package_preserves_prebuilt_resource_binaries(self) -> None:
for variant_name in ("codex", "codex-app-server"):
for target in ("aarch64-apple-darwin", "x86_64-apple-darwin"):