mirror of
https://github.com/openai/codex.git
synced 2026-10-24 18:18:41 +00:00
Include GIO in voice SDKs and native runtimes (#43102)
## Why The Rust GStreamer bindings link GLib's GIO library even when none of the selected plugins imports it. Following only plugin dependencies can omit a library the bindings need. ## What changed - Export `gio-2.0` pkg-config metadata in the voice SDK. - Require GIO as a runtime dependency root on Linux, macOS, and Windows, including its transitive dependencies under the existing import checks. - Reject runtime packages that omit the required GIO library. ## Testing Extend fixtures to cover GIO inclusion and missing-library rejection across platforms, loading GIO and its transitive dependency after moving a Linux runtime, and resolving `gio-2.0` through pkg-config after moving an SDK. GitOrigin-RevId: d12e45d387d56c54d94d788cf07d82bb22c2b4a3
This commit is contained in:
committed by
copyberry
parent
64b482500d
commit
47ca4619be
Vendored
+6
@@ -68,6 +68,12 @@ separate runtime projection and are never copied into users' Codex packages.
|
||||
Cargo/Bazel native providers, final helper linkage and moved-package execution
|
||||
remain separate integration work; exporting an SDK does not enable voice.
|
||||
|
||||
The Rust GStreamer bindings also link GLib's GIO library. Its pkg-config metadata
|
||||
is included in the SDK, and runtime preparation treats its loader identity as an
|
||||
explicit dependency root alongside the seven plugins. GIO comes from the same
|
||||
pinned GLib build; it is not another source package or a GStreamer plugin. Its
|
||||
transitive imports must satisfy the same private-library and system-import checks.
|
||||
|
||||
## Native build recipe
|
||||
|
||||
`build_native.py` runs the unmodified upstream build systems in a new output
|
||||
|
||||
Vendored
+4
-1
@@ -9,7 +9,7 @@ import sys
|
||||
|
||||
# Import only this script's siblings, including under PYTHONSAFEPATH.
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from runtime import Binary, PLUGINS, RuntimeFormat, prepare
|
||||
from runtime import Binary, PLUGINS, RuntimeFormat, prepare, required_library_paths
|
||||
|
||||
SYSTEM_IMPORTS = frozenset(
|
||||
{
|
||||
@@ -138,6 +138,9 @@ def project(prefix, receipts, target, output):
|
||||
inspect,
|
||||
finalize_copy,
|
||||
plugin_dir="lib/gstreamer-1.0",
|
||||
required_libraries=tuple(
|
||||
Path(path).name for path in required_library_paths(target)
|
||||
),
|
||||
)
|
||||
prepare(prefix, receipts, target, output, format)
|
||||
|
||||
|
||||
Vendored
+4
-1
@@ -10,7 +10,7 @@ import sys
|
||||
# Import only this script's siblings, including under PYTHONSAFEPATH.
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from runtime import Binary as MachO
|
||||
from runtime import PLUGINS, RuntimeFormat, digest, prepare
|
||||
from runtime import PLUGINS, RuntimeFormat, digest, prepare, required_library_paths
|
||||
|
||||
SYSTEM_IMPORTS = frozenset(
|
||||
{
|
||||
@@ -150,6 +150,9 @@ def project(prefix, receipts, target, output):
|
||||
SYSTEM_IMPORTS,
|
||||
inspect,
|
||||
finalize_copy,
|
||||
required_libraries=tuple(
|
||||
Path(path).name for path in required_library_paths(target)
|
||||
),
|
||||
)
|
||||
prepare(prefix, receipts, target, output, format)
|
||||
|
||||
|
||||
Vendored
+3
-1
@@ -9,7 +9,7 @@ import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from runtime import PLUGINS, digest
|
||||
from runtime import PLUGINS, digest, required_library_paths
|
||||
|
||||
|
||||
def runtime_files(root: Path, target: str) -> dict[str, str]:
|
||||
@@ -71,5 +71,7 @@ def runtime_files(root: Path, target: str) -> dict[str, str]:
|
||||
files
|
||||
):
|
||||
raise ValueError("runtime must include exactly the selected plugins")
|
||||
if not set(required_library_paths(target)).issubset(files):
|
||||
raise ValueError("runtime must include the required libraries")
|
||||
files["runtime.json"] = hashlib.sha256(data).hexdigest()
|
||||
return files
|
||||
|
||||
Vendored
+20
-1
@@ -20,6 +20,17 @@ PLUGINS = (
|
||||
)
|
||||
|
||||
|
||||
def required_library_paths(target: str) -> tuple[str, ...]:
|
||||
"""Libraries needed by the bindings even when no selected plugin imports them."""
|
||||
if target.endswith("-apple-darwin"):
|
||||
return ("lib/libgio-2.0.0.dylib",)
|
||||
if target.endswith("-unknown-linux-gnu"):
|
||||
return ("lib/libgio-2.0.so.0",)
|
||||
if target.endswith("-pc-windows-msvc"):
|
||||
return ("bin/gio-2.0-0.dll",)
|
||||
raise ValueError("unsupported native runtime target")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Binary:
|
||||
identity: str
|
||||
@@ -37,6 +48,7 @@ class RuntimeFormat:
|
||||
finalize_copy: Callable
|
||||
library_dir: str = "lib"
|
||||
plugin_dir: str = "plugins"
|
||||
required_libraries: tuple[str, ...] = ()
|
||||
|
||||
|
||||
def digest(path):
|
||||
@@ -118,7 +130,14 @@ def prepare(prefix, receipts, target, output, format):
|
||||
binaries[relative] = (record, metadata)
|
||||
identities[metadata.identity] = relative
|
||||
destinations[relative] = destination
|
||||
pending, selected = list(format.plugins), set()
|
||||
libraries = {path.name: relative for relative, path in destinations.items()}
|
||||
if any(name not in libraries for name in format.required_libraries):
|
||||
raise ValueError("missing required native library")
|
||||
pending = [
|
||||
*format.plugins,
|
||||
*(libraries[name] for name in format.required_libraries),
|
||||
]
|
||||
selected = set()
|
||||
while pending:
|
||||
relative = pending.pop()
|
||||
if relative in selected:
|
||||
|
||||
Vendored
+1
@@ -15,6 +15,7 @@ from runtime import digest
|
||||
MODULES = (
|
||||
"glib-2.0",
|
||||
"gobject-2.0",
|
||||
"gio-2.0",
|
||||
"gmodule-no-export-2.0",
|
||||
"gstreamer-1.0",
|
||||
"gstreamer-base-1.0",
|
||||
|
||||
+33
-2
@@ -12,7 +12,8 @@ import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from assemble_package import assemble
|
||||
from runtime import PLUGINS, digest
|
||||
from package_runtime import runtime_files
|
||||
from runtime import PLUGINS, digest, required_library_paths
|
||||
|
||||
|
||||
class AssembleTests(unittest.TestCase):
|
||||
@@ -54,13 +55,19 @@ class AssembleTests(unittest.TestCase):
|
||||
libraries.append(
|
||||
{"path": path.relative_to(root).as_posix(), "sha256": digest(path)}
|
||||
)
|
||||
plugins = [record["path"] for record in libraries]
|
||||
for name in required_library_paths(target):
|
||||
path = root / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(b"prepared required library")
|
||||
libraries.append({"path": name, "sha256": digest(path)})
|
||||
manifest = {
|
||||
"schemaVersion": 1,
|
||||
"developmentOnly": True,
|
||||
"target": target,
|
||||
"sourceCommit": "b" * 40,
|
||||
"sourceManifestSha256": digest(Path(__file__).with_name("sources.json")),
|
||||
"plugins": [record["path"] for record in libraries],
|
||||
"plugins": plugins,
|
||||
"libraries": libraries,
|
||||
}
|
||||
(root / "runtime.json").write_text(json.dumps(manifest))
|
||||
@@ -129,6 +136,30 @@ class AssembleTests(unittest.TestCase):
|
||||
},
|
||||
)
|
||||
|
||||
def test_rejects_stale_runtime_without_gio_for_each_platform(self):
|
||||
for target, plugin, gio in (
|
||||
(
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"lib/gstreamer-1.0/libgst{}.so",
|
||||
"lib/libgio-2.0.so.0",
|
||||
),
|
||||
(
|
||||
"aarch64-apple-darwin",
|
||||
"plugins/libgst{}.dylib",
|
||||
"lib/libgio-2.0.0.dylib",
|
||||
),
|
||||
("aarch64-pc-windows-msvc", "bin/gst{}.dll", "bin/gio-2.0-0.dll"),
|
||||
):
|
||||
with self.subTest(target=target):
|
||||
root, receipt = self.make_runtime(target, plugin)
|
||||
receipt["libraries"] = [
|
||||
r for r in receipt["libraries"] if r["path"] != gio
|
||||
]
|
||||
(root / gio).unlink()
|
||||
(root / "runtime.json").write_text(json.dumps(receipt))
|
||||
with self.assertRaisesRegex(ValueError, "required libraries"):
|
||||
runtime_files(root.resolve(), target)
|
||||
|
||||
def test_rejects_invalid_runtime_receipts_before_creating_package(self):
|
||||
runtime, original = self.make_runtime()
|
||||
changes = [
|
||||
|
||||
+50
-3
@@ -46,6 +46,40 @@ class RuntimeTests(unittest.TestCase):
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
source.write_text("int voice_gio_dependency(void) { return 84; }\n")
|
||||
gio_dependency = self.prefix / "lib/libgiofixture.so.1.2"
|
||||
subprocess.run(
|
||||
[
|
||||
"cc",
|
||||
"-shared",
|
||||
"-fPIC",
|
||||
str(source),
|
||||
"-o",
|
||||
str(gio_dependency),
|
||||
"-Wl,-soname,libgiofixture.so.1",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
source.write_text(
|
||||
"extern int voice_gio_dependency(void); "
|
||||
"int voice_gio_fixture(void) { return voice_gio_dependency(); }\n"
|
||||
)
|
||||
subprocess.run(
|
||||
[
|
||||
"cc",
|
||||
"-shared",
|
||||
"-fPIC",
|
||||
str(source),
|
||||
str(gio_dependency),
|
||||
"-Wl,-rpath,$ORIGIN",
|
||||
"-o",
|
||||
str(self.prefix / "lib/libgio-2.0.so.0.8800.3"),
|
||||
"-Wl,-soname,libgio-2.0.so.0",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
source.write_text(
|
||||
"extern int voice_fixture(void); int voice_plugin(void) { return voice_fixture(); }\n"
|
||||
)
|
||||
@@ -122,7 +156,8 @@ class RuntimeTests(unittest.TestCase):
|
||||
"-c",
|
||||
"import ctypes,json,pathlib,sys; root=pathlib.Path(sys.argv[1]); "
|
||||
"manifest=json.loads((root/'runtime.json').read_text()); "
|
||||
"print([ctypes.CDLL(str(root/p)).voice_plugin() for p in manifest['plugins']])",
|
||||
"print([ctypes.CDLL(str(root/p)).voice_plugin() for p in manifest['plugins']] + "
|
||||
"[ctypes.CDLL(str(root/'lib/libgio-2.0.so.0')).voice_gio_fixture()])",
|
||||
str(moved),
|
||||
],
|
||||
check=True,
|
||||
@@ -131,9 +166,17 @@ class RuntimeTests(unittest.TestCase):
|
||||
env=environment,
|
||||
cwd=self.root,
|
||||
)
|
||||
self.assertEqual(json.loads(result.stdout), [42] * len(PLUGINS))
|
||||
self.assertEqual(json.loads(result.stdout), [42] * len(PLUGINS) + [84])
|
||||
manifest = json.loads((moved / "runtime.json").read_text())
|
||||
self.assertEqual(len(manifest["libraries"]), len(PLUGINS) + 1)
|
||||
self.assertEqual(
|
||||
{record["path"] for record in manifest["libraries"]},
|
||||
{
|
||||
"lib/libfixture.so.1",
|
||||
"lib/libgio-2.0.so.0",
|
||||
"lib/libgiofixture.so.1",
|
||||
*(f"lib/gstreamer-1.0/libgst{name}.so" for name in PLUGINS),
|
||||
},
|
||||
)
|
||||
for record in manifest["libraries"]:
|
||||
path = moved / record["path"]
|
||||
expected = (
|
||||
@@ -141,6 +184,8 @@ class RuntimeTests(unittest.TestCase):
|
||||
if path.parent.name == "gstreamer-1.0"
|
||||
else ()
|
||||
)
|
||||
if path.name == "libgio-2.0.so.0":
|
||||
expected = ("29=$ORIGIN",)
|
||||
self.assertEqual(inspect(path, self.target).rpaths, expected)
|
||||
self.assertEqual(digest(path), record["sourceSha256"])
|
||||
self.assertEqual(record["sha256"], record["sourceSha256"])
|
||||
@@ -161,7 +206,9 @@ class RuntimeTests(unittest.TestCase):
|
||||
|
||||
def test_missing_dependency_and_digest_mismatch_leave_no_output(self):
|
||||
for records in (
|
||||
[r for r in self.inventory if "libgio" not in r["path"]],
|
||||
[r for r in self.inventory if "libfixture" not in r["path"]],
|
||||
[r for r in self.inventory if "libgiofixture" not in r["path"]],
|
||||
[{**r, "sha256": "b" * 64} for r in self.inventory],
|
||||
):
|
||||
with self.subTest(records=records), self.assertRaises(ValueError):
|
||||
|
||||
+14
@@ -47,6 +47,13 @@ class RuntimeTests(unittest.TestCase):
|
||||
capture_output=True,
|
||||
)
|
||||
(self.prefix / "lib/libfixture.1.dylib").symlink_to(self.library.name)
|
||||
gio = self.prefix / "lib/libgio-2.0.0.dylib"
|
||||
shutil.copy2(self.library, gio)
|
||||
subprocess.run(
|
||||
["/usr/bin/install_name_tool", "-id", "@rpath/" + gio.name, str(gio)],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
source.write_text(
|
||||
"extern int voice_fixture(void); int voice_plugin(void) { return voice_fixture(); }\n"
|
||||
)
|
||||
@@ -120,6 +127,7 @@ class RuntimeTests(unittest.TestCase):
|
||||
{r["path"] for r in manifest["libraries"]},
|
||||
{
|
||||
"lib/libfixture.1.dylib",
|
||||
"lib/libgio-2.0.0.dylib",
|
||||
*(f"plugins/libgst{name}.dylib" for name in PLUGINS),
|
||||
},
|
||||
)
|
||||
@@ -205,6 +213,12 @@ class RuntimeTests(unittest.TestCase):
|
||||
def test_missing_dependency_and_duplicate_identity_fail_before_output_creation(
|
||||
self,
|
||||
):
|
||||
self.inventory_path.write_text(
|
||||
json.dumps([r for r in self.inventory if "libgio" not in r["path"]])
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "missing required native library"):
|
||||
project(self.prefix, self.receipts, self.target, self.output)
|
||||
self.assertFalse(self.output.exists())
|
||||
self.inventory_path.write_text(
|
||||
json.dumps([r for r in self.inventory if "libfixture" not in r["path"]])
|
||||
)
|
||||
|
||||
Vendored
+1
@@ -93,6 +93,7 @@ class SdkTests(unittest.TestCase):
|
||||
"--cflags",
|
||||
"--libs",
|
||||
"gstreamer-audio-1.0",
|
||||
"gio-2.0",
|
||||
],
|
||||
env={
|
||||
**os.environ,
|
||||
|
||||
+10
-1
@@ -53,6 +53,7 @@ class RuntimeTests(unittest.TestCase):
|
||||
capture_output=True,
|
||||
cwd=self.root,
|
||||
)
|
||||
shutil.copy2(self.library, self.prefix / "bin/gio-2.0-0.dll")
|
||||
source = self.root / "plugin.c"
|
||||
source.write_text(
|
||||
"__declspec(dllimport) int voice_fixture(void); "
|
||||
@@ -166,12 +167,20 @@ class RuntimeTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(json.loads(result.stdout), [42] * len(PLUGINS))
|
||||
manifest = json.loads((moved / "runtime.json").read_text())
|
||||
self.assertEqual(len(manifest["libraries"]), len(PLUGINS) + 1)
|
||||
self.assertEqual(
|
||||
{record["path"] for record in manifest["libraries"]},
|
||||
{
|
||||
"bin/fixture.dll",
|
||||
"bin/gio-2.0-0.dll",
|
||||
*(f"bin/gst{name}.dll" for name in PLUGINS),
|
||||
},
|
||||
)
|
||||
for record in manifest["libraries"]:
|
||||
self.assertEqual(digest(moved / record["path"]), record["sourceSha256"])
|
||||
|
||||
def test_missing_dependency_and_digest_mismatch_leave_no_output(self):
|
||||
for records in (
|
||||
[r for r in self.inventory if "gio-2.0" not in r["path"]],
|
||||
[r for r in self.inventory if "fixture.dll" not in r["path"]],
|
||||
[{**r, "sha256": "b" * 64} for r in self.inventory],
|
||||
):
|
||||
|
||||
Vendored
+4
-1
@@ -8,7 +8,7 @@ import sys
|
||||
|
||||
# Import only this script's siblings, including under PYTHONSAFEPATH.
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from runtime import Binary, PLUGINS, RuntimeFormat, prepare
|
||||
from runtime import Binary, PLUGINS, RuntimeFormat, prepare, required_library_paths
|
||||
|
||||
# VCRUNTIME140 is a development prerequisite, not a guaranteed Windows component.
|
||||
EXTERNAL_IMPORTS = frozenset(
|
||||
@@ -131,6 +131,9 @@ def project(prefix, receipts, target, output):
|
||||
finalize_copy,
|
||||
library_dir="bin",
|
||||
plugin_dir="bin",
|
||||
required_libraries=tuple(
|
||||
Path(path).name for path in required_library_paths(target)
|
||||
),
|
||||
)
|
||||
prepare(prefix, receipts, target, output, format)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user