1
+ import json
1
2
import pathlib
3
+ from functools import cache
2
4
import os
3
5
import shutil
4
- from typing import Mapping , Sequence
6
+ import time
7
+ from typing import Any , Mapping , Sequence
5
8
from 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
7
10
from signing import (
8
11
CdSigningData ,
9
12
CdSigningType ,
12
15
cd_sign_file ,
13
16
apple_notarize_file ,
14
17
)
15
- from util import info , isDarwin , run_cmd
18
+ from util import info , isDarwin , run_cmd , warn
16
19
from rust import cargo_cmd_name , rust_env , rust_targets
20
+ from importlib import import_module
17
21
18
22
BUILD_DIR_RELATIVE = pathlib .Path (os .environ .get ("BUILD_DIR" ) or "build" )
19
23
BUILD_DIR = BUILD_DIR_RELATIVE .absolute ()
20
24
25
+ REGION = "us-west-2"
26
+ SIGNING_API_BASE_URL = "https://api.signer.builder-tools.aws.dev"
27
+
21
28
22
29
def run_cargo_tests ():
23
30
args = [cargo_cmd_name ()]
@@ -51,14 +58,23 @@ def build_chat_bin(
51
58
52
59
args = [cargo_cmd_name (), "build" , "--locked" , "--package" , package ]
53
60
54
- if release :
55
- args .append ( "--release" )
61
+ for target in targets :
62
+ args .extend ([ "--target" , target ] )
56
63
57
64
if release :
65
+ args .append ("--release" )
58
66
target_subdir = "release"
59
67
else :
60
68
target_subdir = "debug"
61
69
70
+ run_cmd (
71
+ args ,
72
+ env = {
73
+ ** os .environ ,
74
+ ** rust_env (release = release ),
75
+ },
76
+ )
77
+
62
78
# create "universal" binary for macos
63
79
if isDarwin ():
64
80
out_path = BUILD_DIR / f"{ output_name or package } -universal-apple-darwin"
@@ -82,17 +98,201 @@ def build_chat_bin(
82
98
return out_path
83
99
84
100
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 ):
86
278
# First, sign the application
279
+ sign_executable (signing_data , chat_path )
87
280
88
281
# Next, notarize the application
89
282
90
283
# Last, staple the notarization to the application
91
284
pass
92
285
93
286
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
96
296
97
297
98
298
def build_linux (chat_path : pathlib .Path ):
@@ -194,4 +394,24 @@ def build(
194
394
targets = targets ,
195
395
)
196
396
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