upload.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. #!/usr/bin/env python3
  2. import argparse
  3. import datetime
  4. import hashlib
  5. import json
  6. import mmap
  7. import os
  8. import shutil
  9. import subprocess
  10. from struct import Struct
  11. import sys
  12. sys.path.append(
  13. os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + "/../.."))
  14. from zipfile import ZipFile
  15. from lib.config import PLATFORM, get_target_arch, \
  16. get_zip_name, enable_verbose_mode, \
  17. is_verbose_mode, get_platform_key
  18. from lib.util import get_electron_branding, execute, get_electron_version, \
  19. store_artifact, get_electron_exec, get_out_dir, \
  20. SRC_DIR, ELECTRON_DIR, TS_NODE
  21. ELECTRON_VERSION = 'v' + get_electron_version()
  22. PROJECT_NAME = get_electron_branding()['project_name']
  23. PRODUCT_NAME = get_electron_branding()['product_name']
  24. OUT_DIR = get_out_dir()
  25. DIST_NAME = get_zip_name(PROJECT_NAME, ELECTRON_VERSION)
  26. SYMBOLS_NAME = get_zip_name(PROJECT_NAME, ELECTRON_VERSION, 'symbols')
  27. DSYM_NAME = get_zip_name(PROJECT_NAME, ELECTRON_VERSION, 'dsym')
  28. DSYM_SNAPSHOT_NAME = get_zip_name(PROJECT_NAME, ELECTRON_VERSION,
  29. 'dsym-snapshot')
  30. PDB_NAME = get_zip_name(PROJECT_NAME, ELECTRON_VERSION, 'pdb')
  31. DEBUG_NAME = get_zip_name(PROJECT_NAME, ELECTRON_VERSION, 'debug')
  32. TOOLCHAIN_PROFILE_NAME = get_zip_name(PROJECT_NAME, ELECTRON_VERSION,
  33. 'toolchain-profile')
  34. CXX_OBJECTS_NAME = get_zip_name(PROJECT_NAME, ELECTRON_VERSION,
  35. 'libcxx_objects')
  36. def main():
  37. args = parse_args()
  38. if args.verbose:
  39. enable_verbose_mode()
  40. if args.upload_to_storage:
  41. utcnow = datetime.datetime.utcnow()
  42. args.upload_timestamp = utcnow.strftime('%Y%m%d')
  43. build_version = get_electron_build_version()
  44. if not ELECTRON_VERSION.startswith(build_version):
  45. errmsg = f"Tag ({ELECTRON_VERSION}) should match build ({build_version})\n"
  46. sys.stderr.write(errmsg)
  47. sys.stderr.flush()
  48. return 1
  49. tag_exists = False
  50. release = get_release(args.version)
  51. if not release['draft']:
  52. tag_exists = True
  53. if not args.upload_to_storage:
  54. assert release['exists'], \
  55. 'Release does not exist; cannot upload to GitHub!'
  56. assert tag_exists == args.overwrite, \
  57. 'You have to pass --overwrite to overwrite a published release'
  58. # Upload Electron files.
  59. # Rename dist.zip to get_zip_name('electron', version, suffix='')
  60. electron_zip = os.path.join(OUT_DIR, DIST_NAME)
  61. shutil.copy2(os.path.join(OUT_DIR, 'dist.zip'), electron_zip)
  62. upload_electron(release, electron_zip, args)
  63. symbols_zip = os.path.join(OUT_DIR, SYMBOLS_NAME)
  64. shutil.copy2(os.path.join(OUT_DIR, 'symbols.zip'), symbols_zip)
  65. upload_electron(release, symbols_zip, args)
  66. if PLATFORM == 'darwin':
  67. if get_platform_key() == 'darwin' and get_target_arch() == 'x64':
  68. api_path = os.path.join(ELECTRON_DIR, 'electron-api.json')
  69. upload_electron(release, api_path, args)
  70. ts_defs_path = os.path.join(ELECTRON_DIR, 'electron.d.ts')
  71. upload_electron(release, ts_defs_path, args)
  72. dsym_zip = os.path.join(OUT_DIR, DSYM_NAME)
  73. shutil.copy2(os.path.join(OUT_DIR, 'dsym.zip'), dsym_zip)
  74. upload_electron(release, dsym_zip, args)
  75. dsym_snapshot_zip = os.path.join(OUT_DIR, DSYM_SNAPSHOT_NAME)
  76. shutil.copy2(os.path.join(OUT_DIR, 'dsym-snapshot.zip'), dsym_snapshot_zip)
  77. upload_electron(release, dsym_snapshot_zip, args)
  78. elif PLATFORM == 'win32':
  79. pdb_zip = os.path.join(OUT_DIR, PDB_NAME)
  80. shutil.copy2(os.path.join(OUT_DIR, 'pdb.zip'), pdb_zip)
  81. upload_electron(release, pdb_zip, args)
  82. elif PLATFORM == 'linux':
  83. debug_zip = os.path.join(OUT_DIR, DEBUG_NAME)
  84. shutil.copy2(os.path.join(OUT_DIR, 'debug.zip'), debug_zip)
  85. upload_electron(release, debug_zip, args)
  86. # Upload libcxx_objects.zip for linux only
  87. libcxx_objects = get_zip_name('libcxx-objects', ELECTRON_VERSION)
  88. libcxx_objects_zip = os.path.join(OUT_DIR, libcxx_objects)
  89. shutil.copy2(os.path.join(OUT_DIR, 'libcxx_objects.zip'),
  90. libcxx_objects_zip)
  91. upload_electron(release, libcxx_objects_zip, args)
  92. # Upload headers.zip and abi_headers.zip as non-platform specific
  93. if get_target_arch() == "x64":
  94. cxx_headers_zip = os.path.join(OUT_DIR, 'libcxx_headers.zip')
  95. upload_electron(release, cxx_headers_zip, args)
  96. abi_headers_zip = os.path.join(OUT_DIR, 'libcxxabi_headers.zip')
  97. upload_electron(release, abi_headers_zip, args)
  98. # Upload free version of ffmpeg.
  99. ffmpeg = get_zip_name('ffmpeg', ELECTRON_VERSION)
  100. ffmpeg_zip = os.path.join(OUT_DIR, ffmpeg)
  101. ffmpeg_build_path = os.path.join(SRC_DIR, 'out', 'ffmpeg', 'ffmpeg.zip')
  102. shutil.copy2(ffmpeg_build_path, ffmpeg_zip)
  103. upload_electron(release, ffmpeg_zip, args)
  104. chromedriver = get_zip_name('chromedriver', ELECTRON_VERSION)
  105. chromedriver_zip = os.path.join(OUT_DIR, chromedriver)
  106. shutil.copy2(os.path.join(OUT_DIR, 'chromedriver.zip'), chromedriver_zip)
  107. upload_electron(release, chromedriver_zip, args)
  108. mksnapshot = get_zip_name('mksnapshot', ELECTRON_VERSION)
  109. mksnapshot_zip = os.path.join(OUT_DIR, mksnapshot)
  110. if get_target_arch().startswith('arm') and PLATFORM != 'darwin':
  111. # Upload the x64 binary for arm/arm64 mksnapshot
  112. mksnapshot = get_zip_name('mksnapshot', ELECTRON_VERSION, 'x64')
  113. mksnapshot_zip = os.path.join(OUT_DIR, mksnapshot)
  114. shutil.copy2(os.path.join(OUT_DIR, 'mksnapshot.zip'), mksnapshot_zip)
  115. upload_electron(release, mksnapshot_zip, args)
  116. if PLATFORM == 'linux' and get_target_arch() == 'x64':
  117. # Upload the hunspell dictionaries only from the linux x64 build
  118. hunspell_dictionaries_zip = os.path.join(
  119. OUT_DIR, 'hunspell_dictionaries.zip')
  120. upload_electron(release, hunspell_dictionaries_zip, args)
  121. if not tag_exists and not args.upload_to_storage:
  122. # Upload symbols to symbol server.
  123. run_python_upload_script('upload-symbols.py')
  124. if PLATFORM == 'win32':
  125. run_python_upload_script('upload-node-headers.py', '-v', args.version)
  126. if PLATFORM == 'win32':
  127. toolchain_profile_zip = os.path.join(OUT_DIR, TOOLCHAIN_PROFILE_NAME)
  128. with ZipFile(toolchain_profile_zip, 'w') as myzip:
  129. myzip.write(
  130. os.path.join(OUT_DIR, 'windows_toolchain_profile.json'),
  131. 'toolchain_profile.json')
  132. upload_electron(release, toolchain_profile_zip, args)
  133. return 0
  134. def parse_args():
  135. parser = argparse.ArgumentParser(description='upload distribution file')
  136. parser.add_argument('-v', '--version', help='Specify the version',
  137. default=ELECTRON_VERSION)
  138. parser.add_argument('-o', '--overwrite',
  139. help='Overwrite a published release',
  140. action='store_true')
  141. parser.add_argument('-p', '--publish-release',
  142. help='Publish the release',
  143. action='store_true')
  144. parser.add_argument('-s', '--upload_to_storage',
  145. help='Upload assets to azure bucket',
  146. dest='upload_to_storage',
  147. action='store_true',
  148. default=False,
  149. required=False)
  150. parser.add_argument('--verbose',
  151. action='store_true',
  152. help='Mooooorreee logs')
  153. return parser.parse_args()
  154. def run_python_upload_script(script, *args):
  155. script_path = os.path.join(
  156. ELECTRON_DIR, 'script', 'release', 'uploaders', script)
  157. print(execute([sys.executable, script_path] + list(args)))
  158. def get_electron_build_version():
  159. if get_target_arch().startswith('arm') or 'CI' in os.environ:
  160. # In CI we just build as told.
  161. return ELECTRON_VERSION
  162. electron = get_electron_exec()
  163. return subprocess.check_output([electron, '--version']).strip()
  164. class NonZipFileError(ValueError):
  165. """Raised when a given file does not appear to be a zip"""
  166. def zero_zip_date_time(fname):
  167. """ Wrap strip-zip zero_zip_date_time within a file opening operation """
  168. try:
  169. with open(fname, 'r+b') as f:
  170. _zero_zip_date_time(f)
  171. except Exception:
  172. # pylint: disable=W0707
  173. raise NonZipFileError(fname)
  174. def _zero_zip_date_time(zip_):
  175. def purify_extra_data(mm, offset, length, compressed_size=0):
  176. extra_header_struct = Struct("<HH")
  177. # 0. id
  178. # 1. length
  179. STRIPZIP_OPTION_HEADER = 0xFFFF
  180. EXTENDED_TIME_DATA = 0x5455
  181. # Some sort of extended time data, see
  182. # ftp://ftp.info-zip.org/pub/infozip/src/zip30.zip ./proginfo/extrafld.txt
  183. # fallthrough
  184. UNIX_EXTRA_DATA = 0x7875
  185. # Unix extra data; UID / GID stuff, see
  186. # ftp://ftp.info-zip.org/pub/infozip/src/zip30.zip ./proginfo/extrafld.txt
  187. ZIP64_EXTRA_HEADER = 0x0001
  188. zip64_extra_struct = Struct("<HHQQ")
  189. # ZIP64.
  190. # When a ZIP64 extra field is present this 8byte length
  191. # will override the 4byte length defined in canonical zips.
  192. # This is in the form:
  193. # - 0x0001 (header_id)
  194. # - 0x0010 [16] (header_length)
  195. # - ... (8byte uncompressed_length)
  196. # - ... (8byte compressed_length)
  197. mlen = offset + length
  198. while offset < mlen:
  199. values = list(extra_header_struct.unpack_from(mm, offset))
  200. _, header_length = values
  201. extra_struct = Struct("<HH" + "B" * header_length)
  202. values = list(extra_struct.unpack_from(mm, offset))
  203. header_id, header_length = values[:2]
  204. if header_id in (EXTENDED_TIME_DATA, UNIX_EXTRA_DATA):
  205. values[0] = STRIPZIP_OPTION_HEADER
  206. for i in range(2, len(values)):
  207. values[i] = 0xff
  208. extra_struct.pack_into(mm, offset, *values)
  209. if header_id == ZIP64_EXTRA_HEADER:
  210. assert header_length == 16
  211. values = list(zip64_extra_struct.unpack_from(mm, offset))
  212. header_id, header_length, _, compressed_size = values
  213. offset += extra_header_struct.size + header_length
  214. return compressed_size
  215. FILE_HEADER_SIGNATURE = 0x04034b50
  216. CENDIR_HEADER_SIGNATURE = 0x02014b50
  217. archive_size = os.fstat(zip_.fileno()).st_size
  218. signature_struct = Struct("<L")
  219. local_file_header_struct = Struct("<LHHHHHLLLHH")
  220. # 0. L signature
  221. # 1. H version_needed
  222. # 2. H gp_bits
  223. # 3. H compression_method
  224. # 4. H last_mod_time
  225. # 5. H last_mod_date
  226. # 6. L crc32
  227. # 7. L compressed_size
  228. # 8. L uncompressed_size
  229. # 9. H name_length
  230. # 10. H extra_field_length
  231. central_directory_header_struct = Struct("<LHHHHHHLLLHHHHHLL")
  232. # 0. L signature
  233. # 1. H version_made_by
  234. # 2. H version_needed
  235. # 3. H gp_bits
  236. # 4. H compression_method
  237. # 5. H last_mod_time
  238. # 6. H last_mod_date
  239. # 7. L crc32
  240. # 8. L compressed_size
  241. # 9. L uncompressed_size
  242. # 10. H file_name_length
  243. # 11. H extra_field_length
  244. # 12. H file_comment_length
  245. # 13. H disk_number_start
  246. # 14. H internal_attr
  247. # 15. L external_attr
  248. # 16. L rel_offset_local_header
  249. offset = 0
  250. mm = mmap.mmap(zip_.fileno(), 0)
  251. while offset < archive_size:
  252. if signature_struct.unpack_from(mm, offset) != (FILE_HEADER_SIGNATURE,):
  253. break
  254. values = list(local_file_header_struct.unpack_from(mm, offset))
  255. compressed_size, _, name_length, extra_field_length = values[7:11]
  256. # reset last_mod_time
  257. values[4] = 0
  258. # reset last_mod_date
  259. values[5] = 0x21
  260. local_file_header_struct.pack_into(mm, offset, *values)
  261. offset += local_file_header_struct.size + name_length
  262. if extra_field_length != 0:
  263. compressed_size = purify_extra_data(mm, offset, extra_field_length,
  264. compressed_size)
  265. offset += compressed_size + extra_field_length
  266. while offset < archive_size:
  267. if signature_struct.unpack_from(mm, offset) != (CENDIR_HEADER_SIGNATURE,):
  268. break
  269. values = list(central_directory_header_struct.unpack_from(mm, offset))
  270. file_name_length, extra_field_length, file_comment_length = values[10:13]
  271. # reset last_mod_time
  272. values[5] = 0
  273. # reset last_mod_date
  274. values[6] = 0x21
  275. central_directory_header_struct.pack_into(mm, offset, *values)
  276. offset += central_directory_header_struct.size
  277. offset += file_name_length + extra_field_length + file_comment_length
  278. if extra_field_length != 0:
  279. purify_extra_data(mm, offset - extra_field_length, extra_field_length)
  280. if offset == 0:
  281. raise NonZipFileError(zip_.name)
  282. def upload_electron(release, file_path, args):
  283. filename = os.path.basename(file_path)
  284. # Strip zip non determinism before upload, in-place operation
  285. try:
  286. zero_zip_date_time(file_path)
  287. except NonZipFileError:
  288. pass
  289. # if upload_to_storage is set, skip github upload.
  290. # todo (vertedinde): migrate this variable to upload_to_storage
  291. if args.upload_to_storage:
  292. key_prefix = f'release-builds/{args.version}_{args.upload_timestamp}'
  293. store_artifact(os.path.dirname(file_path), key_prefix, [file_path])
  294. upload_sha256_checksum(args.version, file_path, key_prefix)
  295. return
  296. # Upload the file.
  297. upload_io_to_github(release, filename, file_path, args.version)
  298. # Upload the checksum file.
  299. upload_sha256_checksum(args.version, file_path)
  300. def upload_io_to_github(release, filename, filepath, version):
  301. print(f'Uploading {filename} to GitHub')
  302. script_path = os.path.join(
  303. ELECTRON_DIR, 'script', 'release', 'uploaders', 'upload-to-github.ts')
  304. with subprocess.Popen([TS_NODE, script_path, filepath,
  305. filename, str(release['id']), version],
  306. stdout=subprocess.PIPE,
  307. stderr=subprocess.STDOUT) as upload_process:
  308. if is_verbose_mode():
  309. for c in iter(lambda: upload_process.stdout.read(1), b""):
  310. sys.stdout.buffer.write(c)
  311. sys.stdout.flush()
  312. def upload_sha256_checksum(version, file_path, key_prefix=None):
  313. checksum_path = f'{file_path}.sha256sum'
  314. if key_prefix is None:
  315. key_prefix = f'checksums-scratchpad/{version}'
  316. sha256 = hashlib.sha256()
  317. with open(file_path, 'rb') as f:
  318. sha256.update(f.read())
  319. filename = os.path.basename(file_path)
  320. with open(checksum_path, 'w', encoding='utf-8') as checksum:
  321. checksum.write(f'{sha256.hexdigest()} *{filename}')
  322. store_artifact(os.path.dirname(checksum_path), key_prefix, [checksum_path])
  323. def get_release(version):
  324. script_path = os.path.join(
  325. ELECTRON_DIR, 'script', 'release', 'find-github-release.ts')
  326. # Strip warnings from stdout to ensure the only output is the desired object
  327. release_env = os.environ.copy()
  328. release_env['NODE_NO_WARNINGS'] = '1'
  329. release_info = execute([TS_NODE, script_path, version], release_env)
  330. if is_verbose_mode():
  331. print(f'Release info for version: {version}:\n')
  332. print(release_info)
  333. release = json.loads(release_info)
  334. return release
  335. if __name__ == '__main__':
  336. sys.exit(main())