mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
Include voice runtime in Windows portable ZIP packages
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)
|
||||
@@ -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"):
|
||||
|
||||
Reference in New Issue
Block a user