chore: split NPM packages (#11318)

This commit is contained in:
jif-oai
2026-02-10 14:49:53 +00:00
committed by GitHub
parent e57892b211
commit c19969c676
5 changed files with 220 additions and 38 deletions

View File

@@ -593,18 +593,20 @@ jobs:
version="${{ needs.release.outputs.version }}"
tag="${{ needs.release.outputs.tag }}"
mkdir -p dist/npm
patterns=(
"codex-npm-${version}.tgz"
"codex-linux-*-npm-${version}.tgz"
"codex-darwin-*-npm-${version}.tgz"
"codex-win32-*-npm-${version}.tgz"
"codex-responses-api-proxy-npm-${version}.tgz"
"codex-sdk-npm-${version}.tgz"
)
for pattern in "${patterns[@]}"; do
gh release download "$tag" \
--repo "${GITHUB_REPOSITORY}" \
--pattern "codex-npm-${version}.tgz" \
--dir dist/npm
gh release download "$tag" \
--repo "${GITHUB_REPOSITORY}" \
--pattern "codex-responses-api-proxy-npm-${version}.tgz" \
--dir dist/npm
gh release download "$tag" \
--repo "${GITHUB_REPOSITORY}" \
--pattern "codex-sdk-npm-${version}.tgz" \
--pattern "$pattern" \
--dir dist/npm
done
# No NODE_AUTH_TOKEN needed because we use OIDC.
- name: Publish to npm
@@ -618,14 +620,15 @@ jobs:
tag_args+=(--tag "${NPM_TAG}")
fi
tarballs=(
"codex-npm-${VERSION}.tgz"
"codex-responses-api-proxy-npm-${VERSION}.tgz"
"codex-sdk-npm-${VERSION}.tgz"
)
shopt -s nullglob
tarballs=(dist/npm/*-npm-"${VERSION}".tgz)
if [[ ${#tarballs[@]} -eq 0 ]]; then
echo "No npm tarballs found in dist/npm for version ${VERSION}"
exit 1
fi
for tarball in "${tarballs[@]}"; do
npm publish "${GITHUB_WORKSPACE}/dist/npm/${tarball}" "${tag_args[@]}"
npm publish "${GITHUB_WORKSPACE}/${tarball}" "${tag_args[@]}"
done
update-branch:

View File

@@ -3,12 +3,23 @@
import { spawn } from "node:child_process";
import { existsSync } from "fs";
import { createRequire } from "node:module";
import path from "path";
import { fileURLToPath } from "url";
// __dirname equivalent in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const require = createRequire(import.meta.url);
const PLATFORM_PACKAGE_BY_TARGET = {
"x86_64-unknown-linux-musl": "@openai/codex-linux-x64",
"aarch64-unknown-linux-musl": "@openai/codex-linux-arm64",
"x86_64-apple-darwin": "@openai/codex-darwin-x64",
"aarch64-apple-darwin": "@openai/codex-darwin-arm64",
"x86_64-pc-windows-msvc": "@openai/codex-win32-x64",
"aarch64-pc-windows-msvc": "@openai/codex-win32-arm64",
};
const { platform, arch } = process;
@@ -59,9 +70,51 @@ if (!targetTriple) {
throw new Error(`Unsupported platform: ${platform} (${arch})`);
}
const vendorRoot = path.join(__dirname, "..", "vendor");
const archRoot = path.join(vendorRoot, targetTriple);
const platformPackage = PLATFORM_PACKAGE_BY_TARGET[targetTriple];
if (!platformPackage) {
throw new Error(`Unsupported target triple: ${targetTriple}`);
}
const codexBinaryName = process.platform === "win32" ? "codex.exe" : "codex";
const localVendorRoot = path.join(__dirname, "..", "vendor");
const localBinaryPath = path.join(
localVendorRoot,
targetTriple,
"codex",
codexBinaryName,
);
let vendorRoot;
try {
const packageJsonPath = require.resolve(`${platformPackage}/package.json`);
vendorRoot = path.join(path.dirname(packageJsonPath), "vendor");
} catch {
if (existsSync(localBinaryPath)) {
vendorRoot = localVendorRoot;
} else {
const packageManager = detectPackageManager();
const updateCommand =
packageManager === "bun"
? "bun install -g @openai/codex@latest"
: "npm install -g @openai/codex@latest";
throw new Error(
`Missing optional dependency ${platformPackage}. Reinstall Codex: ${updateCommand}`,
);
}
}
if (!vendorRoot) {
const packageManager = detectPackageManager();
const updateCommand =
packageManager === "bun"
? "bun install -g @openai/codex@latest"
: "npm install -g @openai/codex@latest";
throw new Error(
`Missing optional dependency ${platformPackage}. Reinstall Codex: ${updateCommand}`,
);
}
const archRoot = path.join(vendorRoot, targetTriple);
const binaryPath = path.join(archRoot, "codex", codexBinaryName);
// Use an asynchronous spawn instead of spawnSync so that Node is able to

View File

@@ -14,6 +14,9 @@ example, to stage the CLI, responses proxy, and SDK packages for version `0.6.0`
This downloads the native artifacts once, hydrates `vendor/` for each package, and writes
tarballs to `dist/npm/`.
When `--package codex` is provided, the staging helper builds the lightweight
`@openai/codex` meta package plus all `@openai/codex-<platform>` native packages.
If you need to invoke `build_npm_package.py` directly, run
`codex-cli/scripts/install_native_deps.py` first and pass `--vendor-src` pointing to the
directory that contains the populated `vendor/` tree.

View File

@@ -15,14 +15,68 @@ REPO_ROOT = CODEX_CLI_ROOT.parent
RESPONSES_API_PROXY_NPM_ROOT = REPO_ROOT / "codex-rs" / "responses-api-proxy" / "npm"
CODEX_SDK_ROOT = REPO_ROOT / "sdk" / "typescript"
CODEX_PLATFORM_PACKAGES: dict[str, dict[str, str]] = {
"codex-linux-x64": {
"npm_name": "@openai/codex-linux-x64",
"target_triple": "x86_64-unknown-linux-musl",
"os": "linux",
"cpu": "x64",
},
"codex-linux-arm64": {
"npm_name": "@openai/codex-linux-arm64",
"target_triple": "aarch64-unknown-linux-musl",
"os": "linux",
"cpu": "arm64",
},
"codex-darwin-x64": {
"npm_name": "@openai/codex-darwin-x64",
"target_triple": "x86_64-apple-darwin",
"os": "darwin",
"cpu": "x64",
},
"codex-darwin-arm64": {
"npm_name": "@openai/codex-darwin-arm64",
"target_triple": "aarch64-apple-darwin",
"os": "darwin",
"cpu": "arm64",
},
"codex-win32-x64": {
"npm_name": "@openai/codex-win32-x64",
"target_triple": "x86_64-pc-windows-msvc",
"os": "win32",
"cpu": "x64",
},
"codex-win32-arm64": {
"npm_name": "@openai/codex-win32-arm64",
"target_triple": "aarch64-pc-windows-msvc",
"os": "win32",
"cpu": "arm64",
},
}
PACKAGE_EXPANSIONS: dict[str, list[str]] = {
"codex": ["codex", *CODEX_PLATFORM_PACKAGES],
}
PACKAGE_NATIVE_COMPONENTS: dict[str, list[str]] = {
"codex": ["codex", "rg"],
"codex": [],
"codex-linux-x64": ["codex", "rg"],
"codex-linux-arm64": ["codex", "rg"],
"codex-darwin-x64": ["codex", "rg"],
"codex-darwin-arm64": ["codex", "rg"],
"codex-win32-x64": ["codex", "rg", "codex-windows-sandbox-setup", "codex-command-runner"],
"codex-win32-arm64": ["codex", "rg", "codex-windows-sandbox-setup", "codex-command-runner"],
"codex-responses-api-proxy": ["codex-responses-api-proxy"],
"codex-sdk": ["codex"],
}
WINDOWS_ONLY_COMPONENTS: dict[str, list[str]] = {
"codex": ["codex-windows-sandbox-setup", "codex-command-runner"],
PACKAGE_TARGET_FILTERS: dict[str, str] = {
package_name: package_config["target_triple"]
for package_name, package_config in CODEX_PLATFORM_PACKAGES.items()
}
PACKAGE_CHOICES = tuple(PACKAGE_NATIVE_COMPONENTS)
COMPONENT_DEST_DIR: dict[str, str] = {
"codex": "codex",
"codex-responses-api-proxy": "codex-responses-api-proxy",
@@ -36,7 +90,7 @@ def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Build or stage the Codex CLI npm package.")
parser.add_argument(
"--package",
choices=("codex", "codex-responses-api-proxy", "codex-sdk"),
choices=PACKAGE_CHOICES,
default="codex",
help="Which npm package to stage (default: codex).",
)
@@ -98,6 +152,7 @@ def main() -> int:
vendor_src = args.vendor_src.resolve() if args.vendor_src else None
native_components = PACKAGE_NATIVE_COMPONENTS.get(package, [])
target_filter = PACKAGE_TARGET_FILTERS.get(package)
if native_components:
if vendor_src is None:
@@ -108,7 +163,12 @@ def main() -> int:
"pointing to a directory containing pre-installed binaries."
)
copy_native_binaries(vendor_src, staging_dir, package, native_components)
copy_native_binaries(
vendor_src,
staging_dir,
native_components,
target_filter={target_filter} if target_filter else None,
)
if release_version:
staging_dir_str = str(staging_dir)
@@ -125,6 +185,12 @@ def main() -> int:
"Verify the responses API proxy:\n"
f" node {staging_dir_str}/bin/codex-responses-api-proxy.js --help\n\n"
)
elif package in CODEX_PLATFORM_PACKAGES:
print(
f"Staged version {version} for release in {staging_dir_str}\n\n"
"Verify native payload contents:\n"
f" ls {staging_dir_str}/vendor\n\n"
)
else:
print(
f"Staged version {version} for release in {staging_dir_str}\n\n"
@@ -160,6 +226,9 @@ def prepare_staging_dir(staging_dir: Path | None) -> tuple[Path, bool]:
def stage_sources(staging_dir: Path, version: str, package: str) -> None:
package_json: dict
package_json_path: Path | None = None
if package == "codex":
bin_dir = staging_dir / "bin"
bin_dir.mkdir(parents=True, exist_ok=True)
@@ -173,6 +242,33 @@ def stage_sources(staging_dir: Path, version: str, package: str) -> None:
shutil.copy2(readme_src, staging_dir / "README.md")
package_json_path = CODEX_CLI_ROOT / "package.json"
elif package in CODEX_PLATFORM_PACKAGES:
platform_package = CODEX_PLATFORM_PACKAGES[package]
readme_src = REPO_ROOT / "README.md"
if readme_src.exists():
shutil.copy2(readme_src, staging_dir / "README.md")
with open(CODEX_CLI_ROOT / "package.json", "r", encoding="utf-8") as fh:
codex_package_json = json.load(fh)
package_json = {
"name": platform_package["npm_name"],
"version": version,
"license": codex_package_json.get("license", "Apache-2.0"),
"os": [platform_package["os"]],
"cpu": [platform_package["cpu"]],
"files": ["vendor"],
"repository": codex_package_json.get("repository"),
}
engines = codex_package_json.get("engines")
if isinstance(engines, dict):
package_json["engines"] = engines
package_manager = codex_package_json.get("packageManager")
if isinstance(package_manager, str):
package_json["packageManager"] = package_manager
elif package == "codex-responses-api-proxy":
bin_dir = staging_dir / "bin"
bin_dir.mkdir(parents=True, exist_ok=True)
@@ -190,11 +286,20 @@ def stage_sources(staging_dir: Path, version: str, package: str) -> None:
else:
raise RuntimeError(f"Unknown package '{package}'.")
if package_json_path is not None:
with open(package_json_path, "r", encoding="utf-8") as fh:
package_json = json.load(fh)
package_json["version"] = version
if package == "codex-sdk":
if package == "codex":
package_json["files"] = ["bin"]
package_json["optionalDependencies"] = {
CODEX_PLATFORM_PACKAGES[platform_package]["npm_name"]: version
for platform_package in PACKAGE_EXPANSIONS["codex"]
if platform_package != "codex"
}
elif package == "codex-sdk":
scripts = package_json.get("scripts")
if isinstance(scripts, dict):
scripts.pop("prepare", None)
@@ -240,8 +345,8 @@ def stage_codex_sdk_sources(staging_dir: Path) -> None:
def copy_native_binaries(
vendor_src: Path,
staging_dir: Path,
package: str,
components: list[str],
target_filter: set[str] | None = None,
) -> None:
vendor_src = vendor_src.resolve()
if not vendor_src.exists():
@@ -256,15 +361,18 @@ def copy_native_binaries(
shutil.rmtree(vendor_dest)
vendor_dest.mkdir(parents=True, exist_ok=True)
copied_targets: set[str] = set()
for target_dir in vendor_src.iterdir():
if not target_dir.is_dir():
continue
if "windows" in target_dir.name:
components_set.update(WINDOWS_ONLY_COMPONENTS.get(package, []))
if target_filter is not None and target_dir.name not in target_filter:
continue
dest_target_dir = vendor_dest / target_dir.name
dest_target_dir.mkdir(parents=True, exist_ok=True)
copied_targets.add(target_dir.name)
for component in components_set:
dest_dir_name = COMPONENT_DEST_DIR.get(component)
@@ -282,6 +390,12 @@ def copy_native_binaries(
shutil.rmtree(dest_component_dir)
shutil.copytree(src_component_dir, dest_component_dir)
if target_filter is not None:
missing_targets = sorted(target_filter - copied_targets)
if missing_targets:
missing_list = ", ".join(missing_targets)
raise RuntimeError(f"Missing target directories in vendor source: {missing_list}")
def run_npm_pack(staging_dir: Path, output_path: Path) -> Path:
output_path = output_path.resolve()

View File

@@ -25,7 +25,7 @@ if _SPEC is None or _SPEC.loader is None:
_BUILD_MODULE = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(_BUILD_MODULE)
PACKAGE_NATIVE_COMPONENTS = getattr(_BUILD_MODULE, "PACKAGE_NATIVE_COMPONENTS", {})
WINDOWS_ONLY_COMPONENTS = getattr(_BUILD_MODULE, "WINDOWS_ONLY_COMPONENTS", {})
PACKAGE_EXPANSIONS = getattr(_BUILD_MODULE, "PACKAGE_EXPANSIONS", {})
def parse_args() -> argparse.Namespace:
@@ -64,10 +64,19 @@ def collect_native_components(packages: list[str]) -> set[str]:
components: set[str] = set()
for package in packages:
components.update(PACKAGE_NATIVE_COMPONENTS.get(package, []))
components.update(WINDOWS_ONLY_COMPONENTS.get(package, []))
return components
def expand_packages(packages: list[str]) -> list[str]:
expanded: list[str] = []
for package in packages:
for expanded_package in PACKAGE_EXPANSIONS.get(package, [package]):
if expanded_package in expanded:
continue
expanded.append(expanded_package)
return expanded
def resolve_release_workflow(version: str) -> dict:
stdout = subprocess.check_output(
[
@@ -128,14 +137,14 @@ def main() -> int:
runner_temp = Path(os.environ.get("RUNNER_TEMP", tempfile.gettempdir()))
packages = list(args.packages)
packages = expand_packages(list(args.packages))
native_components = collect_native_components(packages)
vendor_temp_root: Path | None = None
vendor_src: Path | None = None
resolved_head_sha: str | None = None
final_messsages = []
final_messages = []
try:
if native_components:
@@ -174,12 +183,12 @@ def main() -> int:
if not args.keep_staging_dirs:
shutil.rmtree(staging_dir, ignore_errors=True)
final_messsages.append(f"Staged {package} at {pack_output}")
final_messages.append(f"Staged {package} at {pack_output}")
finally:
if vendor_temp_root is not None and not args.keep_staging_dirs:
shutil.rmtree(vendor_temp_root, ignore_errors=True)
for msg in final_messsages:
for msg in final_messages:
print(msg)
return 0