11#!/usr/bin/env -S python3 -u
22
33import sys
4-
5- MIN_PYTHON = (3 , 7 )
6- if sys .version_info < MIN_PYTHON :
7- print ("This script requires Python version " + {'.' .join ([str (n ) for n in MIN_PYTHON ])} + " or greater" )
8- sys .exit (1 )
9-
10-
114import argparse
125import subprocess
13- import time
6+ import json
147from pathlib import Path
15- from uuid import uuid4 as uuid
16- import plistlib
17-
188
199
20- def callAlTool (cmd , outPlistPath ):
21- fullCmd = ['xcrun' , 'altool' ] + cmd + ['--output-format' , 'xml' ]
22- with open (outPlistPath , 'wt' ) as outPlist :
23- callRes = subprocess .call (fullCmd , stdout = outPlist )
24- with open (outPlistPath , 'rb' ) as outPlist :
25- try :
26- resp = plistlib .load (outPlist )
27- except plistlib .InvalidFileException :
28- if callRes :
29- print (f"Command '{ fullCmd } ' returned non-zero exit status { callRes } " , file = sys .stderr )
30- else :
31- print (f"output file { outPlistPath } is invalid" , file = sys .stderr )
32- sys .exit (1 )
33- return resp
10+ def callNotaryTool (cmd ):
11+ fullCmd = ['xcrun' , 'notarytool' ] + cmd + ['-f' , 'json' ]
12+ output = subprocess .run (fullCmd , check = True , stdout = subprocess .PIPE ).stdout .decode ('utf-8' )
13+ print (output )
14+ return json .loads (output )
3415
35- def getAlToolErrors (resp ):
36- errors = resp .get ('product-errors' , [])
37- if len (errors ) == 0 :
38- return None
39- return errors
4016
41- def abortOnAlTooolErrors (errors ):
42- if not errors :
43- return
44- for err in errors :
45- print (f"{ err ['code' ]} : { err ['message' ]} " , file = sys .stderr )
46- sys .exit (1 )
47-
48-
49- def notarize (package , username , password ):
17+ def notarize (package , username , team , password ):
5018 print ("Starting..." )
5119 workDir = package .parent
52- uploadId = str (uuid ())
53- print (f"Uploading to Apple to notarize with Bundle ID { uploadId } " )
54- uploadPlistPath = workDir / 'upload-info.plist'
55- uploadResponse = callAlTool ([ '--notarize-app' ,
56- '--primary-bundle-id' , uploadId , '--username' , username , '--password' , password ,
57- '--file' , str (package )
58- ],
59- uploadPlistPath )
60- errors = getAlToolErrors (uploadResponse )
61- abortOnAlTooolErrors (errors )
62- requestId = uploadResponse ['notarization-upload' ]['RequestUUID' ]
63- print (f"Uploading succeeded, Request ID: { requestId } " )
64-
65- success = False
66- startTime = time .time ()
67- while True :
68- time .sleep (30 )
69- print ("Checking progress..." )
70- statusPath = workDir / 'upload-status.plist'
71- infoResponse = callAlTool (['--notarization-info' , requestId , '-u' , username , '-p' , password ],
72- statusPath )
73- errors = getAlToolErrors (infoResponse )
74- if errors and errors [0 ]['code' ] == 1519 : #Could not find the RequestUUID
75- elapsedTime = time .time () - startTime
76- if elapsedTime > 60 * 10 :
77- print ('Timeout waiting for request ID to become valid' )
78- sys .exit (1 )
79- continue
80- abortOnAlTooolErrors (errors )
81- status = infoResponse ['notarization-info' ]['Status' ]
82- print (f"Status: { status } " )
83- if status == 'in progress' :
84- continue
85- if status == 'success' :
86- success = True
87- break
20+ print (f"Uploading to Apple to notarize" )
21+ submission = callNotaryTool (['submit' , str (package ),
22+ '--apple-id' , username , '--team-id' , team , '--password' , password ,
23+ '--wait' ])
24+ success = (submission ['status' ] == 'Accepted' )
25+ submissionId = submission ['id' ]
8826 print ("Downloading log file" )
89- subprocess .check_call (['curl' , '-s' , '-L' , infoResponse ['notarization-info' ]['LogFileURL' ], '-o' , workDir / 'notarization-log.json' ])
90- if not success :
91- log = (workDir / 'notarization-log.json' ).read_text ()
92- print (f"Notarization log:\n { log } " )
27+ callNotaryTool (['log' , submissionId ,
28+ '--apple-id' , username , '--team-id' , team , '--password' , password ,
29+ workDir / 'notarization-log.json' ])
30+ log = (workDir / 'notarization-log.json' ).read_text ()
31+ print (f"Notarization log:\n { log } " )
32+ if not success :
9333 sys .exit (1 )
9434 print ("Stapling" )
9535 subprocess .check_call (['xcrun' , 'stapler' , 'staple' , f"{ package } " ])
9636 print ("Done" )
9737
9838def main ():
9939 parser = argparse .ArgumentParser (description = '''
100- Notarize Mac app
40+ Notarize Mac software
10141''' )
10242 parser .add_argument (dest = 'package' ,
10343 help = f'Package to notarize' )
10444 parser .add_argument ('--user' , dest = 'username' , type = str , required = True ,
10545 help = 'Username' )
10646 parser .add_argument ('--password' , dest = 'password' , type = str , required = True ,
107- help = '''
108- Application password configured for your Apple ID (not your Apple ID password)
109- Alternatively to entering <password> in plaintext, it may also be specified using a '@keychain:'
110- or '@env:' prefix followed by a keychain password item name or environment variable name.
111- Example: '-p @keychain:<name>' uses the password stored in the keychain password item named <name>.
112- Example: '-p @env:<variable>' uses the value in the environment variable named <variable>
113- ''' )
47+ help = 'Application password configured for your Apple ID (not your Apple ID password)' )
48+ parser .add_argument ('--team' , dest = 'team' , type = str , required = True ,
49+ help = 'Team ID' )
11450 args = parser .parse_args ()
115- notarize (Path (args .package ), args .username , args .password )
51+ notarize (Path (args .package ), args .username , args .team , args . password )
11652
11753if __name__ == "__main__" :
11854 main ()
0 commit comments