11#!/usr/bin/env python3
22
3+ import hashlib
34import json
45import os
56from pathlib import Path
67import subprocess
8+ import tarfile
79import tempfile
810import textwrap
911import unittest
@@ -86,6 +88,32 @@ def test_json_like_strings_and_nested_fields_do_not_define_assets(self) -> None:
8688 self .assertIn ("/codex-npm-" , requests [1 ])
8789 self .assertNotIn ("codex-package_SHA256SUMS" , requests [1 ])
8890
91+ def test_macos_install_exposes_code_mode_host_beside_codex (self ) -> None :
92+ with tempfile .TemporaryDirectory () as temp_dir :
93+ root = Path (temp_dir )
94+ archive_path , checksum_path , metadata_json = create_package_release (root )
95+
96+ result , _requests = run_installer_in (
97+ root ,
98+ VERSION ,
99+ metadata_json = metadata_json ,
100+ archive_path = archive_path ,
101+ checksum_path = checksum_path ,
102+ force_macos = True ,
103+ )
104+
105+ self .assertEqual (result .returncode , 0 , result .stderr )
106+ install_bin = root / "install-bin"
107+ current = root / "codex-home" / "packages" / "standalone" / "current"
108+ codex_path = install_bin / "codex"
109+ host_path = install_bin / "codex-code-mode-host"
110+ self .assertEqual (os .readlink (codex_path ), str (current / "bin" / "codex" ))
111+ self .assertEqual (
112+ os .readlink (host_path ),
113+ str (current / "bin" / "codex-code-mode-host" ),
114+ )
115+ self .assertTrue (os .access (host_path , os .X_OK ))
116+
89117
90118def run_installer (
91119 release : str ,
@@ -94,71 +122,169 @@ def run_installer(
94122 metadata_json : str | None = None ,
95123) -> tuple [subprocess .CompletedProcess [str ], list [str ]]:
96124 with tempfile .TemporaryDirectory () as temp_dir :
97- root = Path (temp_dir )
98- bin_dir = root / "bin"
99- bin_dir .mkdir ()
100- request_log = root / "requests.log"
101- fake_curl = bin_dir / "curl"
102- fake_curl .write_text (
103- textwrap .dedent (
104- """\
105- #!/bin/sh
106- url=""
107- for arg in "$@"; do
108- case "$arg" in
109- https://*) url="$arg" ;;
110- esac
111- done
112- printf '%s\n ' "$url" >>"$CODEX_TEST_REQUEST_LOG"
113-
114- case "$url" in
115- https://api.github.com/*)
116- if [ "$CODEX_TEST_METADATA_FAILURE" = "1" ]; then
117- echo "curl: (22) The requested URL returned error: 403" >&2
118- exit 22
119- fi
120- printf '%s\n ' "$CODEX_TEST_METADATA_JSON"
121- ;;
122- *)
123- exit 22
124- ;;
125- esac
126- """
127- ),
128- encoding = "utf-8" ,
125+ return run_installer_in (
126+ Path (temp_dir ),
127+ release ,
128+ metadata_failure = metadata_failure ,
129+ metadata_json = metadata_json ,
129130 )
130- fake_curl .chmod (0o755 )
131-
132- env = os .environ .copy ()
133- env .update (
134- {
135- "CODEX_HOME" : str (root / "codex-home" ),
136- "CODEX_INSTALL_DIR" : str (root / "install-bin" ),
137- "CODEX_NON_INTERACTIVE" : "1" ,
138- "CODEX_RELEASE" : release ,
139- "CODEX_TEST_METADATA_FAILURE" : "1" if metadata_failure else "0" ,
140- "CODEX_TEST_METADATA_JSON" : (
141- metadata_json if metadata_json is not None else release_metadata ()
142- ),
143- "CODEX_TEST_REQUEST_LOG" : str (request_log ),
144- "HOME" : str (root / "home" ),
145- "PATH" : f"{ bin_dir } :/usr/bin:/bin" ,
146- "SHELL" : "/bin/sh" ,
147- }
148- )
149- result = subprocess .run (
150- ["/bin/sh" , str (INSTALL_SCRIPT )],
151- capture_output = True ,
152- check = False ,
153- env = env ,
154- text = True ,
155- )
156- requests = (
157- request_log .read_text (encoding = "utf-8" ).splitlines ()
158- if request_log .exists ()
159- else []
131+
132+
133+ def run_installer_in (
134+ root : Path ,
135+ release : str ,
136+ * ,
137+ metadata_failure : bool = False ,
138+ metadata_json : str | None = None ,
139+ archive_path : Path | None = None ,
140+ checksum_path : Path | None = None ,
141+ force_macos : bool = False ,
142+ ) -> tuple [subprocess .CompletedProcess [str ], list [str ]]:
143+ bin_dir = root / "bin"
144+ bin_dir .mkdir ()
145+ request_log = root / "requests.log"
146+ fake_curl = bin_dir / "curl"
147+ fake_curl .write_text (
148+ textwrap .dedent (
149+ """\
150+ #!/bin/sh
151+ url=""
152+ output=""
153+ previous=""
154+ for arg in "$@"; do
155+ case "$arg" in
156+ https://*) url="$arg" ;;
157+ esac
158+ if [ "$previous" = "-o" ]; then
159+ output="$arg"
160+ fi
161+ previous="$arg"
162+ done
163+ printf '%s\n ' "$url" >>"$CODEX_TEST_REQUEST_LOG"
164+
165+ case "$url" in
166+ https://api.github.com/*)
167+ if [ "$CODEX_TEST_METADATA_FAILURE" = "1" ]; then
168+ echo "curl: (22) The requested URL returned error: 403" >&2
169+ exit 22
170+ fi
171+ printf '%s\n ' "$CODEX_TEST_METADATA_JSON"
172+ ;;
173+ */codex-package_SHA256SUMS)
174+ if [ -n "$CODEX_TEST_CHECKSUM_PATH" ]; then
175+ cp "$CODEX_TEST_CHECKSUM_PATH" "$output"
176+ else
177+ exit 22
178+ fi
179+ ;;
180+ */codex-package-*.tar.gz)
181+ if [ -n "$CODEX_TEST_ARCHIVE_PATH" ]; then
182+ cp "$CODEX_TEST_ARCHIVE_PATH" "$output"
183+ else
184+ exit 22
185+ fi
186+ ;;
187+ *)
188+ exit 22
189+ ;;
190+ esac
191+ """
192+ ),
193+ encoding = "utf-8" ,
194+ )
195+ fake_curl .chmod (0o755 )
196+ if force_macos :
197+ fake_uname = bin_dir / "uname"
198+ fake_uname .write_text (
199+ "#!/bin/sh\n "
200+ 'case "$1" in\n '
201+ " -s) printf 'Darwin\\ n' ;;\n "
202+ " -m) printf 'arm64\\ n' ;;\n "
203+ "esac\n " ,
204+ encoding = "utf-8" ,
160205 )
161- return result , requests
206+ fake_uname .chmod (0o755 )
207+
208+ home = root / "home"
209+ home .mkdir ()
210+ env = os .environ .copy ()
211+ env .update (
212+ {
213+ "CODEX_HOME" : str (root / "codex-home" ),
214+ "CODEX_INSTALL_DIR" : str (root / "install-bin" ),
215+ "CODEX_NON_INTERACTIVE" : "1" ,
216+ "CODEX_RELEASE" : release ,
217+ "CODEX_TEST_ARCHIVE_PATH" : str (archive_path or "" ),
218+ "CODEX_TEST_CHECKSUM_PATH" : str (checksum_path or "" ),
219+ "CODEX_TEST_METADATA_FAILURE" : "1" if metadata_failure else "0" ,
220+ "CODEX_TEST_METADATA_JSON" : (
221+ metadata_json if metadata_json is not None else release_metadata ()
222+ ),
223+ "CODEX_TEST_REQUEST_LOG" : str (request_log ),
224+ "HOME" : str (home ),
225+ "PATH" : f"{ bin_dir } :/usr/bin:/bin" ,
226+ "SHELL" : "/bin/sh" ,
227+ }
228+ )
229+ result = subprocess .run (
230+ ["/bin/sh" , str (INSTALL_SCRIPT )],
231+ capture_output = True ,
232+ check = False ,
233+ env = env ,
234+ text = True ,
235+ )
236+ requests = (
237+ request_log .read_text (encoding = "utf-8" ).splitlines ()
238+ if request_log .exists ()
239+ else []
240+ )
241+ return result , requests
242+
243+
244+ def create_package_release (root : Path ) -> tuple [Path , Path , str ]:
245+ package_dir = root / "package"
246+ (package_dir / "bin" ).mkdir (parents = True )
247+ (package_dir / "codex-path" ).mkdir ()
248+ (package_dir / "codex-package.json" ).write_text ("{}\n " , encoding = "utf-8" )
249+ write_executable (
250+ package_dir / "bin" / "codex" ,
251+ f"#!/bin/sh\n printf 'codex-cli { VERSION } \\ n'\n " ,
252+ )
253+ write_executable (
254+ package_dir / "bin" / "codex-code-mode-host" ,
255+ "#!/bin/sh\n exit 0\n " ,
256+ )
257+ write_executable (package_dir / "codex-path" / "rg" , "#!/bin/sh\n exit 0\n " )
258+
259+ asset = "codex-package-aarch64-apple-darwin.tar.gz"
260+ archive_path = root / asset
261+ with tarfile .open (archive_path , "w:gz" ) as archive :
262+ for path in package_dir .iterdir ():
263+ archive .add (path , arcname = path .name )
264+
265+ archive_digest = hashlib .sha256 (archive_path .read_bytes ()).hexdigest ()
266+ checksum_path = root / "codex-package_SHA256SUMS"
267+ checksum_path .write_text (f"{ archive_digest } { asset } \n " , encoding = "utf-8" )
268+ checksum_digest = hashlib .sha256 (checksum_path .read_bytes ()).hexdigest ()
269+ metadata_json = json .dumps (
270+ {
271+ "assets" : [
272+ {"name" : asset , "digest" : f"sha256:{ archive_digest } " },
273+ {
274+ "name" : "codex-package_SHA256SUMS" ,
275+ "digest" : f"sha256:{ checksum_digest } " ,
276+ },
277+ ],
278+ "tag_name" : f"rust-v{ VERSION } " ,
279+ },
280+ indent = 2 ,
281+ )
282+ return archive_path , checksum_path , metadata_json
283+
284+
285+ def write_executable (path : Path , contents : str ) -> None :
286+ path .write_text (contents , encoding = "utf-8" )
287+ path .chmod (0o755 )
162288
163289
164290def release_metadata (* , compact : bool = False , reorder : bool = False ) -> str :
0 commit comments