1+ import json
12import pathlib
3+ from functools import cache
24import os
35import shutil
4- from typing import Mapping , Sequence
6+ import time
7+ from typing import Any , Mapping , Sequence
58from build import generate_sha
6- from const import CHAT_BINARY_NAME , CHAT_PACKAGE_NAME , LINUX_ARCHIVE_NAME
9+ from const import APPLE_TEAM_ID , CHAT_BINARY_NAME , CHAT_PACKAGE_NAME , LINUX_ARCHIVE_NAME
710from signing import (
811 CdSigningData ,
912 CdSigningType ,
1215 cd_sign_file ,
1316 apple_notarize_file ,
1417)
15- from util import info , isDarwin , run_cmd
18+ from util import info , isDarwin , run_cmd , warn
1619from rust import cargo_cmd_name , rust_env , rust_targets
20+ from importlib import import_module
1721
1822BUILD_DIR_RELATIVE = pathlib .Path (os .environ .get ("BUILD_DIR" ) or "build" )
1923BUILD_DIR = BUILD_DIR_RELATIVE .absolute ()
2024
25+ REGION = "us-west-2"
26+ SIGNING_API_BASE_URL = "https://api.signer.builder-tools.aws.dev"
27+
2128
2229def run_cargo_tests ():
2330 args = [cargo_cmd_name ()]
@@ -51,14 +58,23 @@ def build_chat_bin(
5158
5259 args = [cargo_cmd_name (), "build" , "--locked" , "--package" , package ]
5360
54- if release :
55- args .append ( "--release" )
61+ for target in targets :
62+ args .extend ([ "--target" , target ] )
5663
5764 if release :
65+ args .append ("--release" )
5866 target_subdir = "release"
5967 else :
6068 target_subdir = "debug"
6169
70+ run_cmd (
71+ args ,
72+ env = {
73+ ** os .environ ,
74+ ** rust_env (release = release ),
75+ },
76+ )
77+
6278 # create "universal" binary for macos
6379 if isDarwin ():
6480 out_path = BUILD_DIR / f"{ output_name or package } -universal-apple-darwin"
@@ -82,17 +98,201 @@ def build_chat_bin(
8298 return out_path
8399
84100
85- def sign_and_notarize (chat_path : pathlib .Path ):
101+ @cache
102+ def get_creds ():
103+ boto3 = import_module ("boto3" )
104+ session = boto3 .Session ()
105+ credentials = session .get_credentials ()
106+ creds = credentials .get_frozen_credentials ()
107+ return creds
108+
109+
110+ def cd_signer_request (method : str , path : str , data : str | None = None ):
111+ SigV4Auth = import_module ("botocore.auth" ).SigV4Auth
112+ AWSRequest = import_module ("botocore.awsrequest" ).AWSRequest
113+ requests = import_module ("requests" )
114+
115+ url = f"{ SIGNING_API_BASE_URL } { path } "
116+ headers = {"Content-Type" : "application/json" }
117+ request = AWSRequest (method = method , url = url , data = data , headers = headers )
118+ SigV4Auth (get_creds (), "signer-builder-tools" , REGION ).add_auth (request )
119+
120+ for i in range (1 , 8 ):
121+ response = requests .request (method = method , url = url , headers = dict (request .headers ), data = data )
122+ info (f"CDSigner Request ({ url } ): { response .status_code } " )
123+ if response .status_code == 429 :
124+ warn (f"Too many requests, backing off for { 2 ** i } seconds" )
125+ time .sleep (2 ** i )
126+ continue
127+ return response
128+
129+ raise Exception (f"Failed to request { url } " )
130+
131+
132+ def cd_signer_create_request (manifest : Any ) -> str :
133+ response = cd_signer_request (
134+ method = "POST" ,
135+ path = "/signing_requests" ,
136+ data = json .dumps ({"manifest" : manifest }),
137+ )
138+ response_json = response .json ()
139+ info (f"Signing request create: { response_json } " )
140+ request_id = response_json ["signingRequestId" ]
141+ return request_id
142+
143+
144+ def cd_signer_start_request (request_id : str , source_key : str , destination_key : str , signing_data : CdSigningData ):
145+ response_text = cd_signer_request (
146+ method = "POST" ,
147+ path = f"/signing_requests/{ request_id } /start" ,
148+ data = json .dumps (
149+ {
150+ "iamRole" : f"arn:aws:iam::{ signing_data .aws_account_id } :role/{ signing_data .signing_role_name } " ,
151+ "s3Location" : {
152+ "bucket" : signing_data .bucket_name ,
153+ "sourceKey" : source_key ,
154+ "destinationKey" : destination_key ,
155+ },
156+ }
157+ ),
158+ ).text
159+ info (f"Signing request start: { response_text } " )
160+
161+
162+ def cd_signer_status_request (request_id : str ):
163+ response_json = cd_signer_request (
164+ method = "GET" ,
165+ path = f"/signing_requests/{ request_id } " ,
166+ ).json ()
167+ info (f"Signing request status: { response_json } " )
168+ return response_json ["signingRequest" ]["status" ]
169+
170+
171+ def cd_build_signed_package (file_path : pathlib .Path ):
172+ """
173+ Creates a tarball `package.tar.gz` with the following structure:
174+ ```
175+ package
176+ ├─ manifest.yaml
177+ ├─ artifact
178+ | ├─ EXECUTABLES_TO_SIGN
179+ | | ├─ qchat
180+ ```
181+ """
182+ working_dir = BUILD_DIR / "package"
183+ shutil .rmtree (working_dir , ignore_errors = True )
184+ (BUILD_DIR / "package" / "artifact" / "EXECUTABLES_TO_SIGN" ).mkdir (parents = True )
185+
186+ name = file_path .name
187+
188+ # Write the manifest.yaml
189+ manifest_template_path = pathlib .Path .cwd () / "build-config" / "signing" / "qchat" / "manifest.yaml.template"
190+ (working_dir / "manifest.yaml" ).write_text (manifest_template_path .read_text ().replace ("__NAME__" , name ))
191+
192+ shutil .copy2 (file_path , working_dir / "artifact" / "EXECUTABLES_TO_SIGN" / file_path .name )
193+ file_path .unlink ()
194+
195+ run_cmd (
196+ ["gtar" , "-czf" , BUILD_DIR / "package.tar.gz" , "manifest.yaml" , "artifact" ],
197+ cwd = working_dir ,
198+ )
199+
200+ return BUILD_DIR / "package.tar.gz"
201+
202+
203+ def manifest (
204+ name : str ,
205+ identifier : str ,
206+ ):
207+ """
208+ Creates the required manifest argument when submitting the signing task. This has the same
209+ structure as the manifest.yaml.template under `build-config/signing/qchat/manifest.yaml.template`
210+ """
211+ return {
212+ "type" : "app" ,
213+ "os" : "osx" ,
214+ "name" : name ,
215+ "outputs" : [{"label" : "macos" , "path" : name }],
216+ "app" : {
217+ "identifier" : identifier ,
218+ "signing_requirements" : {
219+ "certificate_type" : "developerIDAppDistribution" ,
220+ "app_id_prefix" : APPLE_TEAM_ID ,
221+ },
222+ },
223+ }
224+
225+
226+ def sign_executable (signing_data : CdSigningData , chat_path : pathlib .Path ):
227+ name = chat_path .name
228+ info (f"Signing { name } " )
229+
230+ info ("Packaging..." )
231+ package_path = cd_build_signed_package (chat_path )
232+
233+ info ("Uploading..." )
234+ run_cmd (["aws" , "s3" , "rm" , "--recursive" , f"s3://{ signing_data .bucket_name } /signed" ])
235+ run_cmd (["aws" , "s3" , "rm" , "--recursive" , f"s3://{ signing_data .bucket_name } /pre-signed" ])
236+ run_cmd (["aws" , "s3" , "cp" , package_path , f"s3://{ signing_data .bucket_name } /pre-signed/package.tar.gz" ])
237+
238+ info ("Sending request..." )
239+ request_id = cd_signer_create_request (manifest (name , "com.amazon.codewhisperer" ))
240+ cd_signer_start_request (
241+ request_id = request_id ,
242+ source_key = "pre-signed/package.tar.gz" ,
243+ destination_key = "signed/signed.zip" ,
244+ signing_data = signing_data ,
245+ )
246+
247+ max_duration = 180
248+ end_time = time .time () + max_duration
249+ i = 1
250+ while True :
251+ info (f"Checking for signed package attempt #{ i } " )
252+ status = cd_signer_status_request (request_id )
253+ info (f"Package has status: { status } " )
254+
255+ match status :
256+ case "success" :
257+ break
258+ case "created" | "processing" | "inProgress" :
259+ pass
260+ case "failure" :
261+ raise RuntimeError ("Signing request failed" )
262+ case _:
263+ warn (f"Unexpected status, ignoring: { status } " )
264+
265+ if time .time () >= end_time :
266+ raise RuntimeError ("Signed package did not appear, check signer logs" )
267+ time .sleep (2 )
268+ i += 1
269+
270+ info ("Signed!" )
271+
272+ info ("Downloading..." )
273+ run_cmd (["aws" , "s3" , "cp" , f"s3://{ signing_data .bucket_name } /signed/signed.zip" , "signed.zip" ])
274+ run_cmd (["unzip" , "signed.zip" ])
275+
276+
277+ def sign_and_notarize (signing_data : CdSigningData , chat_path : pathlib .Path ):
86278 # First, sign the application
279+ sign_executable (signing_data , chat_path )
87280
88281 # Next, notarize the application
89282
90283 # Last, staple the notarization to the application
91284 pass
92285
93286
94- def build_macos (chat_path : pathlib .Path ):
95- sign_and_notarize (chat_path )
287+ def build_macos (chat_path : pathlib .Path , signing_data : CdSigningData | None ):
288+ chat_dst = BUILD_DIR / "qchat"
289+ chat_dst .unlink (missing_ok = True )
290+ shutil .copy2 (chat_path , chat_dst )
291+
292+ if signing_data :
293+ sign_and_notarize (signing_data , chat_dst )
294+
295+ return chat_dst
96296
97297
98298def build_linux (chat_path : pathlib .Path ):
@@ -194,4 +394,24 @@ def build(
194394 targets = targets ,
195395 )
196396
197- pass
397+ if isDarwin ():
398+ if signing_bucket and aws_account_id and apple_id_secret and signing_role_name :
399+ signing_data = CdSigningData (
400+ bucket_name = signing_bucket ,
401+ aws_account_id = aws_account_id ,
402+ notarizing_secret_id = apple_id_secret ,
403+ signing_role_name = signing_role_name ,
404+ )
405+ else :
406+ signing_data = None
407+
408+ chat_path = build_macos (chat_path , signing_data )
409+ sha_path = generate_sha (chat_path )
410+
411+ if output_bucket :
412+ staging_location = f"s3://{ output_bucket } /staging/"
413+ info (f"Build complete, sending to { staging_location } " )
414+ run_cmd (["aws" , "s3" , "cp" , chat_path , staging_location ])
415+ run_cmd (["aws" , "s3" , "cp" , sha_path , staging_location ])
416+ else :
417+ build_linux (chat_path )
0 commit comments