You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Tools/build/JenkinsScripts/distribution/Installer/InstallerAutomation.py

274 lines
14 KiB
Python

#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
import argparse
import os
import re
import shutil
import time
import zipfile
from urllib.parse import urlparse
from urllib.request import urlopen
import BuildInstallerUtils
import PackageExeSigning
import SignTool
import boto3
def getCloudfrontDistDomain(uploadURL):
return urlparse(uploadURL)[1]
def getCloudfrontDistPath(uploadURL):
pathFromDomainName = urlparse(uploadURL)[2]
return pathFromDomainName[1:] # need to remove the first slash, otherwise it will create a nameless directory on S3
def testSigningCredentials(args):
# there are no credentials to test when certName was specified.
if args.certName is not None:
return True
result = SignTool.signtoolTestCredentials(args.signingType,
args.timestampServer,
False)
return result
defaultFilesToSign = ["dev/Tools/LmbrSetup/Win/SetupAssistant.exe",
"dev/Tools/LmbrSetup/Win/SetupAssistantBatch.exe",
"dev/Bin64vc141/ProjectConfigurator.exe",
"dev/Bin64vc141/lmbr.exe",
"dev/Bin64vc141/Lyzard.exe",
"dev/Bin64vc141/Editor.exe",
"dev/Bin64vc142/ProjectConfigurator.exe",
"dev/Bin64vc142/lmbr.exe",
"dev/Bin64vc142/Lyzard.exe",
"dev/Bin64vc142/Editor.exe"]
defaultFilesToSignHelpText = 'Additional files to sign, if signing. (default {})'.format(', '.join(defaultFilesToSign))
def createArgs():
parser = argparse.ArgumentParser(description='Builds the WiX based Lumberyard Installer for Windows.')
parser.add_argument('--packagePath', required=True, help="Path to package, can be url or local.")
parser.add_argument('--workingDir', default="%TEMP%/installerAuto", help="Working directory (default '%%TEMP%%/installerAuto')")
parser.add_argument('--allowedEmptyFolders', default=os.path.join(os.path.dirname(os.path.abspath(__file__)), "allowed_empty_folders.json"), help="The JSON file containing the whitelist of empty folders that we expect in the source package.")
parser.add_argument('--targetURL', required=True, help="Target URL to download the installer from.")
parser.add_argument('--awsProfile', default=None, help='The aws cli profile to use to read from from s3 and cloudfront, and upload to s3. (Default None)') # if on a build machine, it will use the IAM role, if local, it will use [default] in aws credentials file.
parser.add_argument('--lyVersion', default=None, help='Specifies the version used to identify the version of LY installed by this installer. Use of this field will ignore the default behavior of reading the value for this field from the default_settings.json file in the package. (DO NOT USE unless you know what you are doing.)')
parser.add_argument('--suppressVersionInPath', action='store_true', help="Suppresses modification to the target paths with a version (default False)")
parser.add_argument('-bi', '--addBuildIdToPath', action='store_true', help="Add the build version to the target paths prepending to the version of Lumberyard, i.e. buildId/version/installer. (default False)")
parser.add_argument('--privateKey', default=None, help="The signing private key to use to sign the output of this script. Will only attempt to sign if this switch or --certName is specified. Use only one of these two switches. (default None)")
parser.add_argument('--certName', default=None, help="The subject name of the signing certificate to use to sign with. Will only attempt to sign if this switch or --privateKey is specified. Use only one of these two switches. (default None)")
parser.add_argument('-v', '--verbose', action='store_true', help='Enables logging messages (default False)')
parser.add_argument('-k', '--keep', action='store_true', help='Keeps temp files (default False)')
parser.add_argument('--timestampServer', default="http://tsa.starfieldtech.com", help="The timestamp server to use for signing. (default http://tsa.starfieldtech.com)")
parser.add_argument('--filesToSign', nargs='+', default=defaultFilesToSign, help=defaultFilesToSignHelpText)
args, unknown = parser.parse_known_args()
print("Installer automation arguments:")
print(args)
return args
def validateArgs(args):
args.signingPassword = None
assert (os.path.exists(args.allowedEmptyFolders)), 'The whitelist file specified at {} does not exist.'.format(args.allowedEmptyFolders)
# don't allow ambiguity of which way to sign. Have to do this here as we need to only create one SignType to keep in the params object
assert (args.privateKey is None or args.certName is None), "Both a private key and a certificate name was provided, introducing ambiguity. Please only specify one way to sign."
args.signingType = None
if args.privateKey is not None:
# get password for signing
import getpass
args.signingPassword = getpass.getpass("Please provide the signing password: ")
args.signingType = SignTool.KeySigning(args.privateKey, args.signingPassword)
elif args.certName is not None:
args.signingType = SignTool.NameSigning(args.certName)
args.doSigning = args.signingType is not None
if args.doSigning is True:
# Signing requires administration privileges.
import ctypes
try:
is_admin = os.getuid() == 0
except AttributeError:
is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0
assert is_admin, "Administrator privileges must be enabled to sign an installer."
assert (testSigningCredentials(args)), "Signing password is incorrect. Failed to sign and verify test file."
if args.awsProfile:
if args.awsProfile is "": # ANT jobs might pass an empty string to represent None, since ant doesn't have a concept of None or null
args.awsProfile = None
assert (boto3.Session(profile_name=args.awsProfile) is not None), "The AWS CLI profile name specified does not exist on this machine. Please specify an existing AWS CLI profile."
if args.lyVersion:
# check to make sure the value of lyVersion matches the format #.#.#.#
r = re.compile(r"\d+\.\d+\.\d+\.\d+")
assert (r.match(args.lyVersion) is not None), "The value of lyVersion given is not in the form of '<Product>.<Major>.<Minor>.<Patch>'. Please input a version with the correct format."
def run(args):
expandedWorkingDir = os.path.expandvars(args.workingDir)
expandedPackagePath = os.path.expandvars(args.packagePath)
unpackedLocation = os.path.join(expandedWorkingDir, 'unpacked')
fileName = BuildInstallerUtils.get_package_name(expandedPackagePath)
downloadFileOnDisk = os.path.join(expandedWorkingDir, fileName)
# Make sure temp directories exist
BuildInstallerUtils.verbose_print(args.verbose, "Cleaning temp working directories")
if not os.path.exists(expandedWorkingDir):
os.makedirs(expandedWorkingDir)
if os.path.exists(unpackedLocation):
shutil.rmtree(unpackedLocation)
os.makedirs(unpackedLocation)
if os.path.isfile(downloadFileOnDisk):
os.remove(downloadFileOnDisk)
isDownloadFileTemp = False
# 1. Download zip from S3 if it is an URL
if BuildInstallerUtils.is_url(expandedPackagePath):
isDownloadFileTemp = True
BuildInstallerUtils.verbose_print(args.verbose, "Downloading package {}".format(expandedPackagePath))
package = urlopen(expandedPackagePath)
with open(downloadFileOnDisk, 'wb') as output:
output.write(package.read())
elif os.path.isfile(expandedPackagePath):
BuildInstallerUtils.verbose_print(args.verbose, "using on disk package at {}".format(expandedPackagePath))
downloadFileOnDisk = expandedPackagePath
else:
raise Exception('Could not find package "{}" at path {}'.format(fileName, expandedPackagePath))
# 2. Unzip zip file.
BuildInstallerUtils.verbose_print(args.verbose, "Unpacking package to {}".format(unpackedLocation))
z = zipfile.ZipFile(downloadFileOnDisk, "r")
z.extractall(unpackedLocation)
# Preserver file's original timestamp
for f in z.infolist():
name, date_time = f.filename, f.date_time
name = os.path.join(unpackedLocation, name)
date_time = time.mktime(date_time + (0, 0, -1))
os.utime(name, (date_time, date_time))
z.close()
# Sign exes in Lumberyard
if args.privateKey is not None or args.certName is not None:
PackageExeSigning.SignLumberyardExes(unpackedLocation,
args.signingType,
args.timestampServer,
args.verbose,
args.filesToSign)
# 3. Discover Lumberyard version.
buildId = os.path.splitext(fileName)[0]
packageVersion = BuildInstallerUtils.get_ly_version_from_package(args, unpackedLocation)
version = packageVersion
if args.lyVersion:
version = args.lyVersion
BuildInstallerUtils.verbose_print(args.verbose, "Package version is {}, but forcing version to value given for --lyVersion of {}".format(packageVersion, args.lyVersion))
BuildInstallerUtils.verbose_print(args.verbose, "Building installer for Lumberyard v{}".format(version))
# 4. Build installer.
pathToBuild = os.path.join(expandedWorkingDir, version)
targetUrl = BuildInstallerUtils.generate_target_url(args.targetURL, version, buildId, args.suppressVersionInPath, args.addBuildIdToPath)
pathToDirFilelist = os.path.dirname(os.path.realpath(__file__)) + os.sep + 'dir_filelist.json'
# take the name of the package without the file extension to use as the buildId
buildCommand = "python BuildInstaller.py --packageRoot {} " \
"--lyVersion {} " \
"--genRoot {} " \
"--hostURL {} " \
"--allowedEmptyFolders {} " \
"--buildId {} " \
"--dirFilelist {}".format(unpackedLocation, version, pathToBuild, targetUrl,
args.allowedEmptyFolders, buildId, pathToDirFilelist)
if args.verbose:
buildCommand += " -v"
if args.keep:
buildCommand += " --keep"
if args.privateKey is not None:
buildCommand += " --privateKey {} --password {}".format(args.privateKey, args.signingPassword)
elif args.certName is not None:
buildCommand += ' --certName "{}"'.format(args.certName)
if args.doSigning:
buildCommand += " --timestampServer {}".format(args.timestampServer)
BuildInstallerUtils.verbose_print(args.verbose, "Creating build of installer with command:")
BuildInstallerUtils.verbose_print(args.verbose, buildCommand)
build_result = os.system(buildCommand)
assert(build_result == 0), "Running BuildInstaller.py failed with result {}".format(build_result)
BuildInstallerUtils.verbose_print(args.verbose, "Installer creation completed, build is available at {}".format(pathToBuild))
# 5. Upload to the proper S3 bucket
# Get the Cloudfront Distribution ID from the URL we expect to download from (targetUrl)
BuildInstallerUtils.verbose_print(args.verbose, "Beginning upload of installer to S3")
session = boto3.Session(profile_name=args.awsProfile)
client = session.client('cloudfront')
targetDomain = getCloudfrontDistDomain(targetUrl)
distributionList = client.list_distributions()
targetDistId = None
for distribution in distributionList["DistributionList"]["Items"]:
if distribution["DomainName"] == targetDomain:
targetDistId = distribution["Id"]
pass
assert (targetDistId is not None), "No distribution with the domain name {} found.".format(targetDomain)
# Get the s3 bucket info from the Distribution ID, and figure out where we are putting files in the bucket
targetDist = client.get_distribution(Id=targetDistId)
s3Info = targetDist["Distribution"]["DistributionConfig"]["Origins"]["Items"][0]
bucketDomainName = s3Info["DomainName"]
bucketName = bucketDomainName.split('.')[0] # first part of the domain name is the bucket name
BuildInstallerUtils.verbose_print(args.verbose, "S3 bucket associated with targetUrl: {}".format(bucketName))
originPath = s3Info["OriginPath"]
bucketPath = None
if originPath:
# Start originPath after the first character (presumed to be '/') to avoid nameless directory in S3.
bucketPath = '{}/{}'.format(originPath[1:], getCloudfrontDistPath(targetUrl))
else:
bucketPath = getCloudfrontDistPath(targetUrl)
BuildInstallerUtils.verbose_print(args.verbose, "Uploading completed installer to S3 location: {}/{}".format(bucketName, bucketPath))
# Upload each file to the S3 bucket
s3 = session.resource('s3')
s3Bucket = s3.Bucket(bucketName)
installerOutputDir = None
if args.signingType is not None:
installerOutputDir = os.path.join(pathToBuild, "installer")
else:
installerOutputDir = os.path.join(pathToBuild, "unsignedInstaller")
for file in os.listdir(installerOutputDir):
fullFilePath = os.path.join(installerOutputDir, file)
targetBucketPath = '{}/{}'.format(bucketPath, os.path.basename(file))
s3Bucket.upload_file(fullFilePath, targetBucketPath)
if not args.keep:
if os.path.isfile(downloadFileOnDisk) and isDownloadFileTemp:
os.remove(downloadFileOnDisk)
def main():
args = createArgs()
validateArgs(args)
run(args)
if __name__ == "__main__":
main()