Merge branch 'main' into non-uniform-scale-mesh
This commit is contained in:
Vendored
-709
@@ -1,709 +0,0 @@
|
||||
#!/usr/bin/env groovy
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
PIPELINE_CONFIG_FILE = 'AutomatedReview/lumberyard.json'
|
||||
INCREMENTAL_BUILD_SCRIPT_PATH = 'scripts/build/bootstrap/incremental_build_util.py'
|
||||
|
||||
EMPTY_JSON = readJSON text: '{}'
|
||||
|
||||
ENGINE_REPOSITORY_NAME = 'o3de'
|
||||
|
||||
def pipelineProperties = []
|
||||
|
||||
def pipelineParameters = [
|
||||
// Build/clean Parameters
|
||||
// The CLEAN_OUTPUT_DIRECTORY is used by ci_build scripts. Creating the parameter here passes it as an environment variable to jobs and is consumed that way
|
||||
booleanParam(defaultValue: false, description: 'Deletes the contents of the output directory before building. This will cause a \"clean\" build. NOTE: does not imply CLEAN_ASSETS', name: 'CLEAN_OUTPUT_DIRECTORY'),
|
||||
booleanParam(defaultValue: false, description: 'Deletes the contents of the output directories of the AssetProcessor before building.', name: 'CLEAN_ASSETS'),
|
||||
booleanParam(defaultValue: false, description: 'Deletes the contents of the workspace and forces a complete pull.', name: 'CLEAN_WORKSPACE'),
|
||||
booleanParam(defaultValue: false, description: 'Recreates the volume used for the workspace. The volume will be created out of a snapshot taken from main.', name: 'RECREATE_VOLUME'),
|
||||
string(defaultValue: '', description: 'Filters and overrides the list of jobs to run for each of the below platforms (comma-separated). Can\'t be used during a pull request.', name: 'JOB_LIST_OVERRIDE'),
|
||||
|
||||
// Pull Request Parameters
|
||||
string(defaultValue: '', description: '', name: 'DESTINATION_BRANCH'),
|
||||
string(defaultValue: '', description: '', name: 'DESTINATION_COMMIT'),
|
||||
string(defaultValue: '', description: '', name: 'PULL_REQUEST_ID'),
|
||||
string(defaultValue: '', description: '', name: 'REPOSITORY_NAME'),
|
||||
string(defaultValue: '', description: '', name: 'SOURCE_BRANCH'),
|
||||
string(defaultValue: '', description: '', name: 'SOURCE_COMMIT')
|
||||
]
|
||||
|
||||
def palSh(cmd, lbl = '', winSlashReplacement = true) {
|
||||
if (env.IS_UNIX) {
|
||||
sh label: lbl,
|
||||
script: cmd
|
||||
} else if (winSlashReplacement) {
|
||||
bat label: lbl,
|
||||
script: cmd.replace('/','\\')
|
||||
} else {
|
||||
bat label: lbl,
|
||||
script: cmd
|
||||
}
|
||||
}
|
||||
|
||||
def palMkdir(path) {
|
||||
if (env.IS_UNIX) {
|
||||
sh label: "Making directories ${path}",
|
||||
script: "mkdir -p ${path}"
|
||||
} else {
|
||||
def win_path = path.replace('/','\\')
|
||||
bat label: "Making directories ${win_path}",
|
||||
script: "mkdir ${win_path}."
|
||||
}
|
||||
}
|
||||
|
||||
def palRm(path) {
|
||||
if (env.IS_UNIX) {
|
||||
sh label: "Removing ${path}",
|
||||
script: "rm ${path}"
|
||||
} else {
|
||||
def win_path = path.replace('/','\\')
|
||||
bat label: "Removing ${win_path}",
|
||||
script: "del ${win_path}"
|
||||
}
|
||||
}
|
||||
|
||||
def palRmDir(path) {
|
||||
if (env.IS_UNIX) {
|
||||
sh label: "Removing ${path}",
|
||||
script: "rm -rf ${path}"
|
||||
} else {
|
||||
def win_path = path.replace('/','\\')
|
||||
bat label: "Removing ${win_path}",
|
||||
script: "rd /s /q ${win_path}"
|
||||
}
|
||||
}
|
||||
|
||||
def IsJobEnabled(buildTypeMap, pipelineName, platformName) {
|
||||
def job_list_override = params.JOB_LIST_OVERRIDE.tokenize(',')
|
||||
if(params.PULL_REQUEST_ID) { // dont allow pull requests to filter platforms/jobs
|
||||
if(buildTypeMap.value.TAGS) {
|
||||
return buildTypeMap.value.TAGS.contains(pipelineName)
|
||||
}
|
||||
} else if (!job_list_override.isEmpty()) {
|
||||
return params[platformName] && job_list_override.contains(buildTypeMap.key);
|
||||
} else {
|
||||
if (params[platformName]) {
|
||||
if(buildTypeMap.value.TAGS) {
|
||||
return buildTypeMap.value.TAGS.contains(pipelineName)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
def GetRunningPipelineName(JENKINS_JOB_NAME) {
|
||||
// If the job name has an underscore
|
||||
def job_parts = JENKINS_JOB_NAME.tokenize('/')[0].tokenize('_')
|
||||
if (job_parts.size() > 1) {
|
||||
return [job_parts.take(job_parts.size() - 1).join('_'), job_parts[job_parts.size()-1]]
|
||||
}
|
||||
return [job_parts[0], 'default']
|
||||
}
|
||||
|
||||
@NonCPS
|
||||
def RegexMatcher(str, regex) {
|
||||
def matcher = (str =~ regex)
|
||||
return matcher ? matcher.group(1) : null
|
||||
}
|
||||
|
||||
def LoadPipelineConfig(String pipelineName, String branchName, String scmType) {
|
||||
echo 'Loading pipeline config'
|
||||
if (scmType == 'codecommit') {
|
||||
PullFilesFromGit(PIPELINE_CONFIG_FILE, branchName, true, ENGINE_REPOSITORY_NAME)
|
||||
}
|
||||
def pipelineConfig = {}
|
||||
pipelineConfig = readJSON file: PIPELINE_CONFIG_FILE
|
||||
palRm(PIPELINE_CONFIG_FILE)
|
||||
pipelineConfig.platforms = EMPTY_JSON
|
||||
|
||||
// Load the pipeline configs per platform
|
||||
pipelineConfig.PIPELINE_CONFIGS.each { pipeline_config ->
|
||||
def platform_regex = pipeline_config.replace('.','\\.').replace('*', '(.*)')
|
||||
if (!env.IS_UNIX) {
|
||||
platform_regex = platform_regex.replace('/','\\\\')
|
||||
}
|
||||
echo "Downloading platform pipeline configs ${pipeline_config}"
|
||||
if (scmType == 'codecommit') {
|
||||
PullFilesFromGit(pipeline_config, branchName, false, ENGINE_REPOSITORY_NAME)
|
||||
}
|
||||
echo "Searching platform pipeline configs in ${pipeline_config} using ${platform_regex}"
|
||||
for (pipeline_config_path in findFiles(glob: pipeline_config)) {
|
||||
echo "\tFound platform pipeline config ${pipeline_config_path}"
|
||||
def platform = RegexMatcher(pipeline_config_path, platform_regex)
|
||||
if(platform) {
|
||||
pipelineConfig.platforms[platform] = EMPTY_JSON
|
||||
pipelineConfig.platforms[platform].PIPELINE_ENV = readJSON file: pipeline_config_path.toString()
|
||||
}
|
||||
palRm(pipeline_config_path.toString())
|
||||
}
|
||||
}
|
||||
|
||||
// Load the build configs
|
||||
pipelineConfig.BUILD_CONFIGS.each { build_config ->
|
||||
def platform_regex = build_config.replace('.','\\.').replace('*', '(.*)')
|
||||
if (!env.IS_UNIX) {
|
||||
platform_regex = platform_regex.replace('/','\\\\')
|
||||
}
|
||||
echo "Downloading configs ${build_config}"
|
||||
if (scmType == 'codecommit') {
|
||||
PullFilesFromGit(build_config, branchName, false, ENGINE_REPOSITORY_NAME)
|
||||
}
|
||||
echo "Searching configs in ${build_config} using ${platform_regex}"
|
||||
for (build_config_path in findFiles(glob: build_config)) {
|
||||
echo "\tFound config ${build_config_path}"
|
||||
def platform = RegexMatcher(build_config_path, platform_regex)
|
||||
if(platform) {
|
||||
pipelineConfig.platforms[platform].build_types = readJSON file: build_config_path.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
return pipelineConfig
|
||||
}
|
||||
|
||||
def GetSCMType() {
|
||||
def gitUrl = scm.getUserRemoteConfigs()[0].getUrl()
|
||||
if (gitUrl ==~ /https:\/\/git-codecommit.*/) {
|
||||
return 'codecommit'
|
||||
} else if (gitUrl ==~ /https:\/\/github.com.*/) {
|
||||
return 'github'
|
||||
}
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
def GetBuildEnvVars(Map platformEnv, Map buildTypeEnv, String pipelineName) {
|
||||
def envVarMap = [:]
|
||||
platformPipelineEnv = platformEnv['ENV'] ?: [:]
|
||||
platformPipelineEnv.each { var ->
|
||||
envVarMap[var.key] = var.value
|
||||
}
|
||||
platformEnvOverride = platformEnv['PIPELINE_ENV_OVERRIDE'] ?: [:]
|
||||
platformPipelineEnvOverride = platformEnvOverride[pipelineName] ?: [:]
|
||||
platformPipelineEnvOverride.each { var ->
|
||||
envVarMap[var.key] = var.value
|
||||
}
|
||||
buildTypeEnv.each { var ->
|
||||
// This may override the above one if there is an entry defined by the job
|
||||
envVarMap[var.key] = var.value
|
||||
}
|
||||
|
||||
// Environment that only applies to to Jenkins tweaks.
|
||||
// For 3rdParty downloads, we store them in the EBS volume so we can reuse them across node
|
||||
// instances. This allow us to scale up and down without having to re-download 3rdParty
|
||||
envVarMap['LY_PACKAGE_DOWNLOAD_CACHE_LOCATION'] = "${envVarMap['WORKSPACE']}/3rdParty/downloaded_packages"
|
||||
envVarMap['LY_PACKAGE_UNPACK_LOCATION'] = "${envVarMap['WORKSPACE']}/3rdParty/packages"
|
||||
|
||||
return envVarMap
|
||||
}
|
||||
|
||||
def GetEnvStringList(Map envVarMap) {
|
||||
def strList = []
|
||||
envVarMap.each { var ->
|
||||
strList.add("${var.key}=${var.value}")
|
||||
}
|
||||
return strList
|
||||
}
|
||||
|
||||
// Pulls/downloads files from the repo through codecommit. Despite Glob matching is NOT supported, '*' is supported
|
||||
// as a folder or filename (not a portion, it has to be the whole folder or filename)
|
||||
def PullFilesFromGit(String filenamePath, String branchName, boolean failIfNotFound = true, String repositoryName = env.DEFAULT_REPOSITORY_NAME) {
|
||||
echo "PullFilesFromGit filenamePath=${filenamePath} branchName=${branchName} repositoryName=${repositoryName}"
|
||||
def folderPathParts = filenamePath.tokenize('/')
|
||||
def filename = folderPathParts[folderPathParts.size()-1]
|
||||
folderPathParts.remove(folderPathParts.size()-1) // remove the filename
|
||||
def folderPath = folderPathParts.join('/')
|
||||
if (folderPath.contains('*')) {
|
||||
|
||||
def currentPath = ''
|
||||
for (int i = 0; i < folderPathParts.size(); i++) {
|
||||
if (folderPathParts[i] == '*') {
|
||||
palMkdir(currentPath)
|
||||
retry(3) { palSh("aws codecommit get-folder --repository-name ${repositoryName} --commit-specifier ${branchName} --folder-path ${currentPath} > ${currentPath}/.codecommit", "GetFolder ${currentPath}") }
|
||||
def folderInfo = readJSON file: "${currentPath}/.codecommit"
|
||||
folderInfo.subFolders.each { folder ->
|
||||
def newSubPath = currentPath + '/' + folder.relativePath
|
||||
for (int j = i+1; j < folderPathParts.size(); j++) {
|
||||
newSubPath = newSubPath + '/' + folderPathParts[j]
|
||||
}
|
||||
newSubPath = newSubPath + '/' + filename
|
||||
PullFilesFromGit(newSubPath, branchName, false, repositoryName)
|
||||
}
|
||||
palRm("${currentPath}/.codecommit")
|
||||
}
|
||||
if (i == 0) {
|
||||
currentPath = folderPathParts[i]
|
||||
} else {
|
||||
currentPath = currentPath + '/' + folderPathParts[i]
|
||||
}
|
||||
}
|
||||
|
||||
} else if (filename.contains('*')) {
|
||||
|
||||
palMkdir(folderPath)
|
||||
retry(3) { palSh("aws codecommit get-folder --repository-name ${repositoryName} --commit-specifier ${branchName} --folder-path ${folderPath} > ${folderPath}/.codecommit", "GetFolder ${folderPath}") }
|
||||
def folderInfo = readJSON file: "${folderPath}/.codecommit"
|
||||
folderInfo.files.each { file ->
|
||||
PullFilesFromGit("${folderPath}/${filename}", branchName, false, repositoryName)
|
||||
}
|
||||
palRm("${folderPath}/.codecommit")
|
||||
|
||||
} else {
|
||||
|
||||
def errorFile = "${folderPath}/error.txt"
|
||||
palMkdir(folderPath)
|
||||
retry(3) {
|
||||
try {
|
||||
if(env.IS_UNIX) {
|
||||
sh label: "Downloading ${filenamePath}",
|
||||
script: "aws codecommit get-file --repository-name ${repositoryName} --commit-specifier ${branchName} --file-path ${filenamePath} --query fileContent --output text 2>${errorFile} > ${filenamePath}_encoded"
|
||||
sh label: 'Decoding',
|
||||
script: "base64 --decode ${filenamePath}_encoded > ${filenamePath}"
|
||||
} else {
|
||||
errorFile = errorFile.replace('/','\\')
|
||||
win_filenamePath = filenamePath.replace('/', '\\')
|
||||
bat label: "Downloading ${win_filenamePath}",
|
||||
script: "aws codecommit get-file --repository-name ${repositoryName} --commit-specifier ${branchName} --file-path ${filenamePath} --query fileContent --output text 2>${errorFile} > ${win_filenamePath}_encoded"
|
||||
bat label: 'Decoding',
|
||||
script: "certutil -decode ${win_filenamePath}_encoded ${win_filenamePath}"
|
||||
}
|
||||
palRm("${filenamePath}_encoded")
|
||||
} catch (Exception ex) {
|
||||
def error = ''
|
||||
if(fileExists(errorFile)) {
|
||||
error = readFile errorFile
|
||||
}
|
||||
if (!error || !(!failIfNotFound && error.contains('FileDoesNotExistException'))) {
|
||||
palRm("${errorFile} ${filenamePath}.encoded ${filenamePath}")
|
||||
throw new Exception("Could not get file: ${filenamePath}, ex: ${ex}, stderr: ${error}")
|
||||
}
|
||||
}
|
||||
palRm(errorFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def SetLfsCredentials(cmd, lbl = '') {
|
||||
if (env.IS_UNIX) {
|
||||
sh label: lbl,
|
||||
script: cmd
|
||||
} else {
|
||||
bat label: lbl,
|
||||
script: cmd
|
||||
}
|
||||
}
|
||||
|
||||
def CheckoutBootstrapScripts(String branchName) {
|
||||
checkout([$class: "GitSCM",
|
||||
branches: [[name: "*/${branchName}"]],
|
||||
doGenerateSubmoduleConfigurations: false,
|
||||
extensions: [
|
||||
[
|
||||
$class: "SparseCheckoutPaths",
|
||||
sparseCheckoutPaths: [
|
||||
[ $class: "SparseCheckoutPath", path: "AutomatedReview/" ],
|
||||
[ $class: "SparseCheckoutPath", path: "scripts/build/bootstrap/" ],
|
||||
[ $class: "SparseCheckoutPath", path: "Tools/build/JenkinsScripts/build/Platform" ]
|
||||
]
|
||||
],
|
||||
[
|
||||
$class: "CloneOption", depth: 1, noTags: false, reference: "", shallow: true
|
||||
]
|
||||
],
|
||||
submoduleCfg: [],
|
||||
userRemoteConfigs: scm.userRemoteConfigs
|
||||
])
|
||||
}
|
||||
|
||||
def CheckoutRepo(boolean disableSubmodules = false) {
|
||||
dir(ENGINE_REPOSITORY_NAME) {
|
||||
palSh('git lfs uninstall', 'Git LFS Uninstall') // Prevent git from pulling lfs objects during checkout
|
||||
|
||||
if(fileExists('.git')) {
|
||||
// If the repository after checkout is locked, likely we took a snapshot while git was running,
|
||||
// to leave the repo in a usable state, garbagecollect. This also helps in situations where
|
||||
def indexLockFile = '.git/index.lock'
|
||||
if(fileExists(indexLockFile)) {
|
||||
palSh('git gc', 'Git GarbageCollect')
|
||||
}
|
||||
if(fileExists(indexLockFile)) { // if it is still there, remove it
|
||||
palRm(indexLockFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def random = new Random()
|
||||
def retryAttempt = 0
|
||||
retry(5) {
|
||||
if (retryAttempt > 0) {
|
||||
sleep random.nextInt(60 * retryAttempt) // Stagger checkouts to prevent HTTP 429 (Too Many Requests) response from CodeCommit
|
||||
}
|
||||
retryAttempt = retryAttempt + 1
|
||||
if(params.PULL_REQUEST_ID) {
|
||||
// This is a pull request build. Perform merge with destination branch before building.
|
||||
dir(ENGINE_REPOSITORY_NAME) {
|
||||
checkout scm: [
|
||||
$class: 'GitSCM',
|
||||
branches: scm.branches,
|
||||
extensions: [
|
||||
[$class: 'PreBuildMerge', options: [mergeRemote: 'origin', mergeTarget: params.DESTINATION_BRANCH]],
|
||||
[$class: 'SubmoduleOption', disableSubmodules: disableSubmodules, recursiveSubmodules: true],
|
||||
[$class: 'CheckoutOption', timeout: 60]
|
||||
],
|
||||
userRemoteConfigs: scm.userRemoteConfigs
|
||||
]
|
||||
}
|
||||
} else {
|
||||
dir(ENGINE_REPOSITORY_NAME) {
|
||||
checkout scm: [
|
||||
$class: 'GitSCM',
|
||||
branches: scm.branches,
|
||||
extensions: [
|
||||
[$class: 'SubmoduleOption', disableSubmodules: disableSubmodules, recursiveSubmodules: true],
|
||||
[$class: 'CheckoutOption', timeout: 60]
|
||||
],
|
||||
userRemoteConfigs: scm.userRemoteConfigs
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add folder where we will store the 3rdParty downloads and packages
|
||||
if(!fileExists('3rdParty')) {
|
||||
palMkdir('3rdParty')
|
||||
}
|
||||
|
||||
dir(ENGINE_REPOSITORY_NAME) {
|
||||
// Run lfs in a separate step. Jenkins is unable to load the credentials for the custom LFS endpoint
|
||||
withCredentials([usernamePassword(credentialsId: "${env.GITHUB_USER}", passwordVariable: 'accesstoken', usernameVariable: 'username')]) {
|
||||
SetLfsCredentials("git config -f .lfsconfig lfs.url https://${username}:${accesstoken}@${env.LFS_URL}", 'Set credentials')
|
||||
}
|
||||
palSh('git lfs install', 'Git LFS Install')
|
||||
palSh('git lfs pull', 'Git LFS Pull')
|
||||
|
||||
// CHANGE_ID is used by some scripts to identify uniquely the current change (usually metric jobs)
|
||||
palSh('git rev-parse HEAD > commitid', 'Getting commit id')
|
||||
env.CHANGE_ID = readFile file: 'commitid'
|
||||
env.CHANGE_ID = env.CHANGE_ID.trim()
|
||||
palRm('commitid')
|
||||
}
|
||||
}
|
||||
|
||||
def PreBuildCommonSteps(Map pipelineConfig, String projectName, String pipeline, String branchName, String platform, String buildType, String workspace, boolean mount = true, boolean disableSubmodules = false) {
|
||||
echo 'Starting pre-build common steps...'
|
||||
|
||||
if (mount) {
|
||||
unstash name: 'incremental_build_script'
|
||||
|
||||
def pythonCmd = ''
|
||||
if(env.IS_UNIX) pythonCmd = 'sudo -E python -u '
|
||||
else pythonCmd = 'python -u '
|
||||
|
||||
if(env.RECREATE_VOLUME.toBoolean()) {
|
||||
palSh("${pythonCmd} ${INCREMENTAL_BUILD_SCRIPT_PATH} --action delete --project ${projectName} --pipeline ${pipeline} --branch ${branchName} --platform ${platform} --build_type ${buildType}", 'Deleting volume')
|
||||
}
|
||||
timeout(5) {
|
||||
palSh("${pythonCmd} ${INCREMENTAL_BUILD_SCRIPT_PATH} --action mount --project ${projectName} --pipeline ${pipeline} --branch ${branchName} --platform ${platform} --build_type ${buildType}", 'Mounting volume')
|
||||
}
|
||||
|
||||
if(env.IS_UNIX) {
|
||||
sh label: 'Setting volume\'s ownership',
|
||||
script: """
|
||||
if sudo test ! -d "${workspace}"; then
|
||||
sudo mkdir -p ${workspace}
|
||||
cd ${workspace}/..
|
||||
sudo chown -R lybuilder:root .
|
||||
fi
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup previous repo location, we are currently at the root of the workspace, if we have a .git folder
|
||||
// we need to cleanup. Once all branches take this relocation, we can remove this
|
||||
if(env.CLEAN_WORKSPACE.toBoolean() || fileExists("${workspace}/.git")) {
|
||||
if(fileExists(workspace)) {
|
||||
palRmDir(workspace)
|
||||
}
|
||||
}
|
||||
|
||||
dir(workspace) {
|
||||
|
||||
CheckoutRepo(disableSubmodules)
|
||||
|
||||
// Get python
|
||||
dir(ENGINE_REPOSITORY_NAME) {
|
||||
if(env.IS_UNIX) {
|
||||
sh label: 'Getting python',
|
||||
script: 'python/get_python.sh'
|
||||
} else {
|
||||
bat label: 'Getting python',
|
||||
script: 'python/get_python.bat'
|
||||
}
|
||||
|
||||
if(env.CLEAN_OUTPUT_DIRECTORY.toBoolean() || env.CLEAN_ASSETS.toBoolean()) {
|
||||
def command = "${pipelineConfig.BUILD_ENTRY_POINT} --platform ${platform} --type clean"
|
||||
if (env.IS_UNIX) {
|
||||
sh label: "Running ${platform} clean",
|
||||
script: "${pipelineConfig.PYTHON_DIR}/python.sh -u ${command}"
|
||||
} else {
|
||||
bat label: "Running ${platform} clean",
|
||||
script: "${pipelineConfig.PYTHON_DIR}/python.cmd -u ${command}".replace('/','\\')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def Build(Map options, String platform, String type, String workspace) {
|
||||
def command = "${options.BUILD_ENTRY_POINT} --platform ${platform} --type ${type}"
|
||||
dir("${workspace}/${ENGINE_REPOSITORY_NAME}") {
|
||||
if (env.IS_UNIX) {
|
||||
sh label: "Running ${platform} ${type}",
|
||||
script: "${options.PYTHON_DIR}/python.sh -u ${command}"
|
||||
} else {
|
||||
bat label: "Running ${platform} ${type}",
|
||||
script: "${options.PYTHON_DIR}/python.cmd -u ${command}".replace('/','\\')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def TestMetrics(Map options, String workspace, String branchName, String repoName, String buildJobName, String outputDirectory, String configuration) {
|
||||
catchError(buildResult: null, stageResult: null) {
|
||||
def cmakeBuildDir = [workspace, ENGINE_REPOSITORY_NAME, outputDirectory].join('/')
|
||||
dir("${workspace}/${ENGINE_REPOSITORY_NAME}") {
|
||||
checkout scm: [
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: '*/main']],
|
||||
extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'mars']],
|
||||
userRemoteConfigs: [[url: "${env.MARS_REPO}", name: 'mars', credentialsId: "${env.GITHUB_USER}"]]
|
||||
]
|
||||
withCredentials([usernamePassword(credentialsId: "${env.SERVICE_USER}", passwordVariable: 'apitoken', usernameVariable: 'username')]) {
|
||||
def command = "${options.PYTHON_DIR}/python.cmd -u mars/scripts/python/ctest_test_metric_scraper.py -e jenkins.creds.user ${username} -e jenkins.creds.pass ${apitoken} ${cmakeBuildDir} ${branchName} %BUILD_NUMBER% AR ${configuration} ${repoName} "
|
||||
bat label: "Publishing ${buildJobName} Test Metrics",
|
||||
script: command
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def PostBuildCommonSteps(String workspace, boolean mount = true) {
|
||||
echo 'Starting post-build common steps...'
|
||||
|
||||
if(params.PULL_REQUEST_ID) {
|
||||
dir("${workspace}/${ENGINE_REPOSITORY_NAME}") {
|
||||
if(fileExists('.git')) {
|
||||
palSh('git reset --hard HEAD', 'Discard PR merge, git reset')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mount) {
|
||||
def pythonCmd = ''
|
||||
if(env.IS_UNIX) pythonCmd = 'sudo -E python -u '
|
||||
else pythonCmd = 'python -u '
|
||||
|
||||
try {
|
||||
timeout(5) {
|
||||
palSh("${pythonCmd} ${INCREMENTAL_BUILD_SCRIPT_PATH} --action unmount", 'Unmounting volume')
|
||||
}
|
||||
} catch (Exception e) {
|
||||
echo "Unmount script error ${e}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def CreateSetupStage(Map pipelineConfig, String projectName, String pipelineName, String branchName, String platformName, String jobName, Map environmentVars) {
|
||||
return {
|
||||
stage("Setup") {
|
||||
PreBuildCommonSteps(pipelineConfig, projectName, pipelineName, branchName, platformName, jobName, environmentVars['WORKSPACE'], environmentVars['MOUNT_VOLUME'])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def CreateBuildStage(Map pipelineConfig, String platformName, String jobName, Map environmentVars) {
|
||||
return {
|
||||
stage("${jobName}") {
|
||||
Build(pipelineConfig, platformName, jobName, environmentVars['WORKSPACE'])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def CreateTestMetricsStage(Map pipelineConfig, String branchName, Map environmentVars, String buildJobName, String outputDirectory, String configuration) {
|
||||
return {
|
||||
stage("${buildJobName}_metrics") {
|
||||
TestMetrics(pipelineConfig, environmentVars['WORKSPACE'], branchName, env.DEFAULT_REPOSITORY_NAME, buildJobName, outputDirectory, configuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def CreateTeardownStage(Map environmentVars) {
|
||||
return {
|
||||
stage("Teardown") {
|
||||
PostBuildCommonSteps(environmentVars['WORKSPACE'], environmentVars['MOUNT_VOLUME'])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def projectName = ''
|
||||
def pipelineName = ''
|
||||
def branchName = ''
|
||||
def pipelineConfig = {}
|
||||
|
||||
// Start Pipeline
|
||||
try {
|
||||
stage('Setup Pipeline') {
|
||||
node('controller') {
|
||||
def envVarList = []
|
||||
if(isUnix()) {
|
||||
envVarList.add('IS_UNIX=1')
|
||||
}
|
||||
withEnv(envVarList) {
|
||||
timestamps {
|
||||
(projectName, pipelineName) = GetRunningPipelineName(env.JOB_NAME) // env.JOB_NAME is the name of the job given by Jenkins
|
||||
scmType = GetSCMType()
|
||||
|
||||
if(env.BRANCH_NAME) {
|
||||
branchName = env.BRANCH_NAME
|
||||
} else {
|
||||
branchName = scm.branches[0].name // for non-multibranch pipelines
|
||||
env.BRANCH_NAME = branchName // so scripts that read this environment have it (e.g. incremental_build_util.py)
|
||||
}
|
||||
pipelineProperties.add(disableConcurrentBuilds())
|
||||
|
||||
echo "Running \"${pipelineName}\" for \"${branchName}\"..."
|
||||
|
||||
if (scmType == 'github') {
|
||||
CheckoutBootstrapScripts(branchName)
|
||||
}
|
||||
|
||||
// Load configs
|
||||
pipelineConfig = LoadPipelineConfig(pipelineName, branchName, scmType)
|
||||
|
||||
// Add each platform as a parameter that the user can disable if needed
|
||||
pipelineConfig.platforms.each { platform ->
|
||||
pipelineParameters.add(booleanParam(defaultValue: true, description: '', name: platform.key))
|
||||
}
|
||||
pipelineProperties.add(parameters(pipelineParameters))
|
||||
properties(pipelineProperties)
|
||||
|
||||
// Stash the INCREMENTAL_BUILD_SCRIPT_PATH since all nodes will use it
|
||||
if (scmType == 'codecommit') {
|
||||
PullFilesFromGit(INCREMENTAL_BUILD_SCRIPT_PATH, branchName, true, ENGINE_REPOSITORY_NAME)
|
||||
}
|
||||
stash name: 'incremental_build_script',
|
||||
includes: INCREMENTAL_BUILD_SCRIPT_PATH
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(env.BUILD_NUMBER == '1') {
|
||||
// Exit pipeline early on the intial build. This allows Jenkins to load the pipeline for the branch and enables users
|
||||
// to select build parameters on their first actual build. See https://issues.jenkins.io/browse/JENKINS-41929
|
||||
currentBuild.result = 'SUCCESS'
|
||||
return
|
||||
}
|
||||
|
||||
// Build and Post-Build Testing Stage
|
||||
def buildConfigs = [:]
|
||||
|
||||
// Platform Builds run on EC2
|
||||
pipelineConfig.platforms.each { platform ->
|
||||
platform.value.build_types.each { build_job ->
|
||||
if (IsJobEnabled(build_job, pipelineName, platform.key)) { // User can filter jobs, jobs are tagged by pipeline
|
||||
def envVars = GetBuildEnvVars(platform.value.PIPELINE_ENV ?: EMPTY_JSON, build_job.value.PIPELINE_ENV ?: EMPTY_JSON, pipelineName)
|
||||
envVars['JOB_NAME'] = "${branchName}_${platform.key}_${build_job.key}" // backwards compatibility, some scripts rely on this
|
||||
def nodeLabel = envVars['NODE_LABEL']
|
||||
|
||||
buildConfigs["${platform.key} [${build_job.key}]"] = {
|
||||
node("${nodeLabel}") {
|
||||
if(isUnix()) { // Has to happen inside a node
|
||||
envVars['IS_UNIX'] = 1
|
||||
}
|
||||
withEnv(GetEnvStringList(envVars)) {
|
||||
timeout(time: envVars['TIMEOUT'], unit: 'MINUTES', activity: true) {
|
||||
try {
|
||||
def build_job_name = build_job.key
|
||||
|
||||
CreateSetupStage(pipelineConfig, projectName, pipelineName, branchName, platform.key, build_job.key, envVars).call()
|
||||
|
||||
if(build_job.value.steps) { //this is a pipe with many steps so create all the build stages
|
||||
build_job.value.steps.each { build_step ->
|
||||
build_job_name = build_step
|
||||
CreateBuildStage(pipelineConfig, platform.key, build_step, envVars).call()
|
||||
}
|
||||
} else {
|
||||
CreateBuildStage(pipelineConfig, platform.key, build_job.key, envVars).call()
|
||||
}
|
||||
|
||||
if (env.MARS_REPO && platform.key == 'Windows' && build_job_name.startsWith('test')) {
|
||||
def output_directory = platform.value.build_types[build_job_name].PARAMETERS.OUTPUT_DIRECTORY
|
||||
def configuration = platform.value.build_types[build_job_name].PARAMETERS.CONFIGURATION
|
||||
CreateTestMetricsStage(pipelineConfig, branchName, envVars, build_job_name, output_directory, configuration).call()
|
||||
}
|
||||
}
|
||||
catch(Exception e) {
|
||||
// https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/hudson/model/Result.java
|
||||
// {SUCCESS,UNSTABLE,FAILURE,NOT_BUILT,ABORTED}
|
||||
def currentResult = envVars['ON_FAILURE_MARK'] ?: 'FAILURE'
|
||||
if (currentResult == 'FAILURE') {
|
||||
currentBuild.result = 'FAILURE'
|
||||
error "FAILURE: ${e}"
|
||||
} else if (currentResult == 'UNSTABLE') {
|
||||
currentBuild.result = 'UNSTABLE'
|
||||
unstable(message: "UNSTABLE: ${e}")
|
||||
}
|
||||
}
|
||||
finally {
|
||||
CreateTeardownStage(envVars).call()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timestamps {
|
||||
|
||||
stage('Build') {
|
||||
parallel buildConfigs // Run parallel builds
|
||||
}
|
||||
|
||||
echo 'All builds successful'
|
||||
}
|
||||
}
|
||||
catch(Exception e) {
|
||||
error "Exception: ${e}"
|
||||
}
|
||||
finally {
|
||||
try {
|
||||
if(env.SNS_TOPIC) {
|
||||
snsPublish(
|
||||
topicArn: env.SNS_TOPIC,
|
||||
subject:'Build Result',
|
||||
message:"${currentBuild.currentResult}:${params.REPOSITORY_NAME}:${params.SOURCE_BRANCH}:${params.SOURCE_COMMIT}:${params.DESTINATION_COMMIT}:${params.PULL_REQUEST_ID}:${BUILD_URL}:${env.RECREATE_VOLUME}:${env.CLEAN_OUTPUT_DIRECTORY}:${env.CLEAN_ASSETS}"
|
||||
)
|
||||
}
|
||||
step([
|
||||
$class: 'Mailer',
|
||||
notifyEveryUnstableBuild: true,
|
||||
sendToIndividuals: true,
|
||||
recipients: emailextrecipients([
|
||||
[$class: 'CulpritsRecipientProvider'],
|
||||
[$class: 'RequesterRecipientProvider']
|
||||
])
|
||||
])
|
||||
} catch(Exception e) {
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"BUILD_ENTRY_POINT": "Tools/build/JenkinsScripts/build/ci_build.py",
|
||||
"PIPELINE_CONFIGS": [
|
||||
"Tools/build/JenkinsScripts/build/Platform/*/pipeline.json",
|
||||
"restricted/*/Tools/build/JenkinsScripts/build/pipeline.json"
|
||||
],
|
||||
"BUILD_CONFIGS": [
|
||||
"Tools/build/JenkinsScripts/build/Platform/*/build_config.json",
|
||||
"restricted/*/Tools/build/JenkinsScripts/build/build_config.json"
|
||||
],
|
||||
"PYTHON_DIR": "python"
|
||||
}
|
||||
@@ -9,12 +9,38 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
#
|
||||
|
||||
file(READ "${CMAKE_CURRENT_LIST_DIR}/project.json" project_json)
|
||||
#! Adds the --project-path argument to the VS IDE debugger command arguments
|
||||
function(add_vs_debugger_arguments)
|
||||
# Inject the project root into the --project-path argument into the Visual Studio Debugger arguments by defaults
|
||||
list(APPEND app_targets AutomatedTesting.GameLauncher AutomatedTesting.ServerLauncher)
|
||||
list(APPEND app_targets AssetBuilder AssetProcessor AssetProcessorBatch Editor)
|
||||
foreach(app_target IN LISTS app_targets)
|
||||
if (TARGET ${app_target})
|
||||
set_property(TARGET ${app_target} APPEND PROPERTY VS_DEBUGGER_COMMAND_ARGUMENTS "--project-path=\"${CMAKE_CURRENT_LIST_DIR}\"")
|
||||
endif()
|
||||
endforeach()
|
||||
endfunction()
|
||||
|
||||
string(JSON project_target_name ERROR_VARIABLE json_error GET ${project_json} "project_name")
|
||||
if(${json_error})
|
||||
message(FATAL_ERROR "Unable to read key 'project_name' from 'project.json'")
|
||||
endif()
|
||||
if(NOT PROJECT_NAME)
|
||||
cmake_minimum_required(VERSION 3.19)
|
||||
project(AutomatedTesting
|
||||
LANGUAGES C CXX
|
||||
VERSION 1.0.0.0
|
||||
)
|
||||
include(EngineFinder.cmake OPTIONAL)
|
||||
find_package(o3de REQUIRED)
|
||||
o3de_initialize()
|
||||
add_vs_debugger_arguments()
|
||||
else()
|
||||
# Add the project_name to global LY_PROJECTS_TARGET_NAME property
|
||||
file(READ "${CMAKE_CURRENT_LIST_DIR}/project.json" project_json)
|
||||
|
||||
set_property(GLOBAL APPEND PROPERTY LY_PROJECTS_TARGET_NAME ${project_target_name})
|
||||
add_subdirectory(Gem)
|
||||
string(JSON project_target_name ERROR_VARIABLE json_error GET ${project_json} "project_name")
|
||||
if(json_error)
|
||||
message(FATAL_ERROR "Unable to read key 'project_name' from 'project.json'")
|
||||
endif()
|
||||
|
||||
set_property(GLOBAL APPEND PROPERTY LY_PROJECTS_TARGET_NAME ${project_target_name})
|
||||
|
||||
add_subdirectory(Gem)
|
||||
endif()
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
// Please read README.md for an explanation on why scenesrg.srgi and viewsrg.srgi are
|
||||
// located in this folder (And how you can optionally customize your own scenesrg.srgi
|
||||
// and viewsrg.srgi in your game project).
|
||||
|
||||
#include <Atom/Features/SrgSemantics.azsli>
|
||||
|
||||
partial ShaderResourceGroup RayTracingSceneSrg : SRG_RayTracingScene
|
||||
{
|
||||
/* Intentionally Empty. Helps define the SrgSemantic for RayTracingSceneSrg once.*/
|
||||
};
|
||||
|
||||
#define AZ_COLLECTING_PARTIAL_SRGS
|
||||
#include <Atom/Feature/Common/Assets/ShaderResourceGroups/RayTracingSceneSrgAll.azsli>
|
||||
#undef AZ_COLLECTING_PARTIAL_SRGS
|
||||
@@ -922,7 +922,7 @@ void CVars::Init()
|
||||
"Will not render CGFs past the given amount of drawcalls\n"
|
||||
"(<=0 off (default), >0 draw calls limit)");
|
||||
|
||||
REGISTER_CVAR(e_CheckOctreeObjectsBoxSize, 1, VF_NULL, "CryWarning for crazy sized COctreeNode m_objectsBoxes");
|
||||
REGISTER_CVAR(e_CheckOctreeObjectsBoxSize, 1, VF_NULL, "Warning for crazy sized COctreeNode m_objectsBoxes");
|
||||
REGISTER_CVAR(e_DebugGeomPrep, 0, VF_NULL, "enable logging of Geom preparation");
|
||||
DefineConstIntCVar(e_GeomCaches, 1, VF_NULL, "Activates drawing of geometry caches");
|
||||
REGISTER_CVAR(e_GeomCacheBufferSize, 128, VF_CHEAT, "Geometry cache stream buffer upper limit size in MB. Default: 128");
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace AZ
|
||||
}
|
||||
|
||||
AZ::OSString msgBoxMessage;
|
||||
msgBoxMessage.append("CrySystem could not initialize correctly for the following reason(s):");
|
||||
msgBoxMessage.append("O3DE could not initialize correctly for the following reason(s):");
|
||||
|
||||
for (const AZ::OSString& errMsg : m_errorStringsCollected)
|
||||
{
|
||||
@@ -47,7 +47,7 @@ namespace AZ
|
||||
Trace::Output(nullptr, msgBoxMessage.c_str());
|
||||
Trace::Output(nullptr, "\n==================================================================\n");
|
||||
|
||||
EBUS_EVENT(AZ::NativeUI::NativeUIRequestBus, DisplayOkDialog, "CrySystem Initialization Failed", msgBoxMessage.c_str(), false);
|
||||
EBUS_EVENT(AZ::NativeUI::NativeUIRequestBus, DisplayOkDialog, "O3DE Initialization Failed", msgBoxMessage.c_str(), false);
|
||||
}
|
||||
} // namespace Debug
|
||||
} // namespace AZ
|
||||
|
||||
@@ -605,7 +605,7 @@ void CSystem::DebugStats([[maybe_unused]] bool checkpoint, [[maybe_unused]] bool
|
||||
{
|
||||
if (!dbgmodules[i].handle)
|
||||
{
|
||||
CryLogAlways("WARNING: <CrySystem> CSystem::DebugStats: NULL handle for %s", dbgmodules[i].name.c_str());
|
||||
CryLogAlways("WARNING: CSystem::DebugStats: NULL handle for %s", dbgmodules[i].name.c_str());
|
||||
nolib++;
|
||||
continue;
|
||||
}
|
||||
@@ -642,7 +642,7 @@ void CSystem::DebugStats([[maybe_unused]] bool checkpoint, [[maybe_unused]] bool
|
||||
}
|
||||
else
|
||||
{
|
||||
CryLogAlways("WARNING: <CrySystem> CSystem::DebugStats: could not retrieve function from DLL %s", dbgmodules[i].name.c_str());
|
||||
CryLogAlways("WARNING: CSystem::DebugStats: could not retrieve function from DLL %s", dbgmodules[i].name.c_str());
|
||||
nolib++;
|
||||
};
|
||||
#endif
|
||||
@@ -1066,7 +1066,7 @@ void CSystem::FatalError(const char* format, ...)
|
||||
|
||||
if (szSysErrorMessage)
|
||||
{
|
||||
CryLogAlways("<CrySystem> Last System Error: %s", szSysErrorMessage);
|
||||
CryLogAlways("Last System Error: %s", szSysErrorMessage);
|
||||
}
|
||||
|
||||
if (GetUserCallback())
|
||||
|
||||
@@ -1117,7 +1117,7 @@ namespace AZ
|
||||
return asset;
|
||||
}
|
||||
|
||||
void AssetManager::UpdateDebugStatus(AZ::Data::Asset<AZ::Data::AssetData> asset)
|
||||
void AssetManager::UpdateDebugStatus(const AZ::Data::Asset<AZ::Data::AssetData>& asset)
|
||||
{
|
||||
if(!m_debugAssetEvents)
|
||||
{
|
||||
|
||||
@@ -358,7 +358,7 @@ namespace AZ
|
||||
|
||||
Asset<AssetData> GetAssetInternal(const AssetId& assetId, const AssetType& assetType, AssetLoadBehavior assetReferenceLoadBehavior, const AssetLoadParameters& loadParams = AssetLoadParameters{}, AssetInfo assetInfo = AssetInfo(), bool signalLoaded = false);
|
||||
|
||||
void UpdateDebugStatus(AZ::Data::Asset<AZ::Data::AssetData> asset);
|
||||
void UpdateDebugStatus(const AZ::Data::Asset<AZ::Data::AssetData>& asset);
|
||||
|
||||
/**
|
||||
* Gets a root asset and dependencies as individual async loads if necessary.
|
||||
|
||||
@@ -42,9 +42,8 @@ namespace AZ
|
||||
}
|
||||
}
|
||||
|
||||
#define AZ_TRACE_METHOD_NAME_CATEGORY(name, category) AZ::Debug::EventTrace::ScopedSlice AZ_JOIN(ScopedSlice__, __LINE__)(name, category);
|
||||
|
||||
#ifdef AZ_PROFILE_TELEMETRY
|
||||
# define AZ_TRACE_METHOD_NAME_CATEGORY(name, category) AZ::Debug::EventTrace::ScopedSlice AZ_JOIN(ScopedSlice__, __LINE__)(name, category);
|
||||
# define AZ_TRACE_METHOD_NAME(name) \
|
||||
AZ_TRACE_METHOD_NAME_CATEGORY(name, "") \
|
||||
AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzTrace, name)
|
||||
@@ -53,6 +52,7 @@ namespace AZ
|
||||
AZ_TRACE_METHOD_NAME_CATEGORY(AZ_FUNCTION_SIGNATURE, "") \
|
||||
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzTrace)
|
||||
#else
|
||||
# define AZ_TRACE_METHOD_NAME_CATEGORY(name, category)
|
||||
# define AZ_TRACE_METHOD_NAME(name) AZ_TRACE_METHOD_NAME_CATEGORY(name, "")
|
||||
# define AZ_TRACE_METHOD() AZ_TRACE_METHOD_NAME(AZ_FUNCTION_SIGNATURE)
|
||||
#endif
|
||||
|
||||
@@ -617,9 +617,9 @@ namespace AzFramework
|
||||
// won't free the mutex until the load is complete.
|
||||
// So instead, queue the notification until the next tick, so that it doesn't occur within the AssetCatalogRequestBus mutex, and also
|
||||
// so that the entire AssetCatalog initialization is complete.
|
||||
AZ::TickBus::QueueFunction([catalogRegistryFile]()
|
||||
AZ::TickBus::QueueFunction([catalogRegistryString = AZStd::string(catalogRegistryFile)]()
|
||||
{
|
||||
AssetCatalogEventBus::Broadcast(&AssetCatalogEventBus::Events::OnCatalogLoaded, catalogRegistryFile);
|
||||
AssetCatalogEventBus::Broadcast(&AssetCatalogEventBus::Events::OnCatalogLoaded, catalogRegistryString.c_str());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace AzToolsFramework
|
||||
|
||||
if (!contextMenu.m_menu->isEmpty())
|
||||
{
|
||||
contextMenu.m_menu->popup(QCursor::pos());
|
||||
contextMenu.m_menu->exec(QCursor::pos());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,7 +313,7 @@ namespace O3DELauncher
|
||||
return "Failed to initialize the CrySystem Interface";
|
||||
|
||||
case ReturnCode::ErrCryEnvironment:
|
||||
return "Failed to initialize the CryEngine global environment";
|
||||
return "Failed to initialize the global environment";
|
||||
|
||||
case ReturnCode::ErrAssetProccessor:
|
||||
return "Failed to connect to AssetProcessor while the /Amazon/AzCore/Bootstrap/wait_for_connect value is 1\n."
|
||||
|
||||
+10
-10
@@ -39,15 +39,15 @@ namespace
|
||||
}
|
||||
|
||||
|
||||
@interface LumberyardApplicationDelegate_iOS : NSObject<UIApplicationDelegate>
|
||||
@interface O3DEApplicationDelegate_iOS : NSObject<UIApplicationDelegate>
|
||||
{
|
||||
}
|
||||
@end // LumberyardApplicationDelegate_iOS Interface
|
||||
@end // O3DEApplicationDelegate_iOS Interface
|
||||
|
||||
@implementation LumberyardApplicationDelegate_iOS
|
||||
@implementation O3DEApplicationDelegate_iOS
|
||||
|
||||
|
||||
- (int)runLumberyardApplication
|
||||
- (int)runO3DEApplication
|
||||
{
|
||||
#if AZ_TESTS_ENABLED
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace
|
||||
return static_cast<int>(ReturnCode::ErrUnitTestNotSupported);
|
||||
|
||||
#else
|
||||
using namespace LumberyardLauncher;
|
||||
using namespace O3DELauncher;
|
||||
|
||||
PlatformMainInfo mainInfo;
|
||||
mainInfo.m_updateResourceLimits = IncreaseResourceLimits;
|
||||
@@ -79,19 +79,19 @@ namespace
|
||||
#endif // AZ_TESTS_ENABLED
|
||||
}
|
||||
|
||||
- (void)launchLumberyardApplication
|
||||
- (void)launchO3DEApplication
|
||||
{
|
||||
const int exitCode = [self runLumberyardApplication];
|
||||
const int exitCode = [self runO3DEApplication];
|
||||
exit(exitCode);
|
||||
}
|
||||
|
||||
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
|
||||
{
|
||||
// prevent the lumberyard runtime from running when launched in a xctest environment, otherwise the
|
||||
// prevent the o3de runtime from running when launched in a xctest environment, otherwise the
|
||||
// testing framework will kill the "app" due to the lengthy bootstrap process
|
||||
if ([[NSProcessInfo processInfo] environment][@"XCTestConfigurationFilePath"] == nil)
|
||||
{
|
||||
[self performSelector:@selector(launchLumberyardApplication) withObject:nil afterDelay:0.0];
|
||||
[self performSelector:@selector(launchO3DEApplication) withObject:nil afterDelay:0.0];
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
@@ -132,4 +132,4 @@ namespace
|
||||
&AzFramework::IosLifecycleEvents::Bus::Events::OnDidReceiveMemoryWarning);
|
||||
}
|
||||
|
||||
@end // LumberyardApplicationDelegate_iOS Implementation
|
||||
@end // O3DEApplicationDelegate_iOS Implementation
|
||||
+4
-4
@@ -16,12 +16,12 @@
|
||||
#include <AzFramework/Input/Buses/Notifications/RawInputNotificationBus_Platform.h>
|
||||
|
||||
|
||||
@interface LumberyardApplication_iOS : UIApplication
|
||||
@interface O3DEApplication_iOS : UIApplication
|
||||
{
|
||||
}
|
||||
@end // LumberyardApplication_iOS Interface
|
||||
@end // O3DEApplication_iOS Interface
|
||||
|
||||
@implementation LumberyardApplication_iOS
|
||||
@implementation O3DEApplication_iOS
|
||||
|
||||
- (void)touchesBegan: (NSSet<UITouch*>*)touches withEvent: (UIEvent*)event
|
||||
{
|
||||
@@ -65,4 +65,4 @@
|
||||
[self touchesEnded: touches withEvent: event];
|
||||
}
|
||||
|
||||
@end // LumberyardApplication_iOS Implementation
|
||||
@end // O3DEApplication_iOS Implementation
|
||||
@@ -13,8 +13,8 @@ set(FILES
|
||||
Launcher_iOS.mm
|
||||
Launcher_Traits_iOS.h
|
||||
Launcher_Traits_Platform.h
|
||||
LumberyardApplication_iOS.mm
|
||||
LumberyardApplicationDelegate_iOS.mm
|
||||
O3DEApplication_iOS.mm
|
||||
O3DEApplicationDelegate_iOS.mm
|
||||
../Common/Apple/Launcher_Apple.mm
|
||||
../Common/Apple/Launcher_Apple.h
|
||||
../Common/UnixLike/Launcher_UnixLike.cpp
|
||||
|
||||
@@ -18,6 +18,21 @@ foreach(project_name project_path IN ZIP_LISTS LY_PROJECTS_TARGET_NAME LY_PROJEC
|
||||
# If the project_path is relative, it is evaluated relative to the ${LY_ROOT_FOLDER}
|
||||
# Otherwise the the absolute project_path is returned with symlinks resolved
|
||||
file(REAL_PATH ${project_path} project_real_path BASE_DIRECTORY ${LY_ROOT_FOLDER})
|
||||
if(NOT project_name)
|
||||
if(NOT EXISTS ${project_real_path}/project.json)
|
||||
message(FATAL_ERROR "The specified project path of ${project_real_path} does not contain a project.json file")
|
||||
else()
|
||||
# Add the project_name to global LY_PROJECTS_TARGET_NAME property
|
||||
file(READ "${project_real_path}/project.json" project_json)
|
||||
string(JSON project_name ERROR_VARIABLE json_error GET ${project_json} "project_name")
|
||||
if(json_error)
|
||||
message(FATAL_ERROR "There is an error reading the \"project_name\" key from the '${project_real_path}/project.json' file: ${json_error}")
|
||||
endif()
|
||||
message(WARNING "The project located at path ${project_real_path} has a valid \"project name\" of '${project_name}' read from it's project.json file."
|
||||
" This indicates that the ${project_real_path}/CMakeLists.txt is not properly appending the \"project name\" "
|
||||
"to the LY_PROJECTS_TARGET_NAME global property. Other configuration errors might occur")
|
||||
endif()
|
||||
endif()
|
||||
################################################################################
|
||||
# Monolithic game
|
||||
################################################################################
|
||||
|
||||
@@ -5702,7 +5702,7 @@ extern "C" int AZ_DLL_EXPORT CryEditMain(int argc, char* argv[])
|
||||
int exitCode = 0;
|
||||
|
||||
BOOL didCryEditStart = CCryEditApp::instance()->InitInstance();
|
||||
AZ_Error("Editor", didCryEditStart, "CryEditor did not initialize correctly, and will close."
|
||||
AZ_Error("Editor", didCryEditStart, "O3DE Editor did not initialize correctly, and will close."
|
||||
"\nThis could be because of incorrectly configured components, or missing required gems."
|
||||
"\nSee other errors for more details.");
|
||||
|
||||
|
||||
@@ -1931,7 +1931,7 @@ void CCryEditDoc::Fetch(const QString& holdName, const QString& relativeHoldPath
|
||||
if (!LoadXmlArchiveArray(arrXmlAr, holdFilename, holdPath))
|
||||
{
|
||||
QMessageBox::critical(QApplication::activeWindow(), "Error", "The temporary 'Hold' level failed to load successfully. Your level might be corrupted, you should restart the Editor.", QMessageBox::Ok);
|
||||
AZ_Error("CryEditDoc", false, "Fetch failed to load the Xml Archive");
|
||||
AZ_Error("EditDoc", false, "Fetch failed to load the Xml Archive");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -342,7 +342,6 @@ bool LegacyViewportCameraControllerInstance::HandleInputChannelEvent(const AzFra
|
||||
m_inRotateMode = true;
|
||||
}
|
||||
|
||||
shouldConsumeEvent = true;
|
||||
shouldCaptureCursor = true;
|
||||
}
|
||||
else if (state == InputChannel::State::Ended)
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
#include <AzFramework/API/AtomActiveInterface.h>
|
||||
#include <AzCore/Console/IConsole.h>
|
||||
|
||||
AZ_CVAR(bool, ed_useAtomNativeViewport, false, nullptr, AZ::ConsoleFunctorFlags::Null, "Use the new Atom-native Editor viewport (experimental, not yet stable");
|
||||
AZ_CVAR(bool, ed_useAtomNativeViewport, true, nullptr, AZ::ConsoleFunctorFlags::Null, "Use the new Atom-native Editor viewport (experimental, not yet stable");
|
||||
|
||||
bool CViewManager::IsMultiViewportEnabled()
|
||||
{
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
|
||||
#include <QApplication>
|
||||
|
||||
static const auto ManipulatorPriority = AzFramework::ViewportControllerPriority::Highest;
|
||||
static const auto InteractionPriority = AzFramework::ViewportControllerPriority::High;
|
||||
static const auto ManipulatorPriority = AzFramework::ViewportControllerPriority::High;
|
||||
static const auto InteractionPriority = AzFramework::ViewportControllerPriority::Low;
|
||||
|
||||
namespace SandboxEditor
|
||||
{
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ namespace ImageProcessingAtom
|
||||
// EBusTraits overrides
|
||||
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
|
||||
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
|
||||
typedef AZStd::recursive_mutex MutexType;
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Loads an image from a source file path
|
||||
|
||||
@@ -102,7 +102,8 @@ namespace AZ
|
||||
// Register Shader Resource Group Layout Builder
|
||||
AssetBuilderSDK::AssetBuilderDesc srgLayoutBuilderDescriptor;
|
||||
srgLayoutBuilderDescriptor.m_name = "Shader Resource Group Layout Builder";
|
||||
srgLayoutBuilderDescriptor.m_version = 52; // ATOM-14780
|
||||
srgLayoutBuilderDescriptor.m_version = 53; // ATOM-15196
|
||||
|
||||
srgLayoutBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern("*.azsl", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard));
|
||||
srgLayoutBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern("*.azsli", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard));
|
||||
srgLayoutBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern(AZStd::string::format("*.%s", SrgLayoutBuilder::MergedPartialSrgsExtension), AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard));
|
||||
@@ -117,7 +118,7 @@ namespace AZ
|
||||
// Register Shader Asset Builder
|
||||
AssetBuilderSDK::AssetBuilderDesc shaderAssetBuilderDescriptor;
|
||||
shaderAssetBuilderDescriptor.m_name = "Shader Asset Builder";
|
||||
shaderAssetBuilderDescriptor.m_version = 96; // SPEC-6065
|
||||
shaderAssetBuilderDescriptor.m_version = 97; // ATOM-15196
|
||||
// .shader file changes trigger rebuilds
|
||||
shaderAssetBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern( AZStd::string::format("*.%s", RPI::ShaderSourceData::Extension), AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard));
|
||||
shaderAssetBuilderDescriptor.m_busId = azrtti_typeid<ShaderAssetBuilder>();
|
||||
@@ -132,7 +133,7 @@ namespace AZ
|
||||
shaderVariantAssetBuilderDescriptor.m_name = "Shader Variant Asset Builder";
|
||||
// Both "Shader Variant Asset Builder" and "Shader Asset Builder" produce ShaderVariantAsset products. If you update
|
||||
// ShaderVariantAsset you will need to update BOTH version numbers, not just "Shader Variant Asset Builder".
|
||||
shaderVariantAssetBuilderDescriptor.m_version = 17; // SPEC-6065
|
||||
shaderVariantAssetBuilderDescriptor.m_version = 18; // ATOM-15196
|
||||
shaderVariantAssetBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern(AZStd::string::format("*.%s", RPI::ShaderVariantListSourceData::Extension), AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard));
|
||||
shaderVariantAssetBuilderDescriptor.m_busId = azrtti_typeid<ShaderVariantAssetBuilder>();
|
||||
shaderVariantAssetBuilderDescriptor.m_createJobFunction = AZStd::bind(&ShaderVariantAssetBuilder::CreateJobs, &m_shaderVariantAssetBuilder, AZStd::placeholders::_1, AZStd::placeholders::_2);
|
||||
|
||||
@@ -646,8 +646,8 @@ namespace AZ
|
||||
return 0; // Nothing to draw.
|
||||
}
|
||||
|
||||
auto vertexBuffer = RPI::DynamicDrawInterface::Get()->GetDynamicBuffer(totalVtxBufferSize);
|
||||
auto indexBuffer = RPI::DynamicDrawInterface::Get()->GetDynamicBuffer(totalIdxBufferSize);
|
||||
auto vertexBuffer = RPI::DynamicDrawInterface::Get()->GetDynamicBuffer(totalVtxBufferSize, RHI::Alignment::InputAssembly);
|
||||
auto indexBuffer = RPI::DynamicDrawInterface::Get()->GetDynamicBuffer(totalIdxBufferSize, RHI::Alignment::InputAssembly);
|
||||
|
||||
if (!vertexBuffer || !indexBuffer)
|
||||
{
|
||||
|
||||
@@ -66,6 +66,7 @@ namespace AZ
|
||||
// load the RayTracingSceneSrg asset
|
||||
Data::Asset<RPI::ShaderResourceGroupAsset> rayTracingSceneSrgAsset =
|
||||
RPI::AssetUtils::LoadAssetByProductPath<RPI::ShaderResourceGroupAsset>("shaderlib/raytracingscenesrg_raytracingscenesrg.azsrg", RPI::AssetUtils::TraceLevel::Error);
|
||||
AZ_Assert(rayTracingSceneSrgAsset.IsReady(), "Failed to load RayTracingSceneSrg asset");
|
||||
|
||||
m_rayTracingSceneSrg = RPI::ShaderResourceGroup::Create(rayTracingSceneSrgAsset);
|
||||
}
|
||||
|
||||
@@ -30,38 +30,42 @@ namespace AZ
|
||||
{
|
||||
None = 0,
|
||||
|
||||
/// Supports input assembly access through a IndexBufferView or StreamBufferView.
|
||||
/// Supports input assembly access through a IndexBufferView or StreamBufferView. This flag is for buffers that are not updated often
|
||||
InputAssembly = AZ_BIT(0),
|
||||
|
||||
|
||||
/// Supports input assembly access through a IndexBufferView or StreamBufferView. This flag is for buffers that are updated frequently
|
||||
DynamicInputAssembly = AZ_BIT(1),
|
||||
|
||||
/// Supports constant access through a ShaderResourceGroup.
|
||||
Constant = AZ_BIT(1),
|
||||
Constant = AZ_BIT(2),
|
||||
|
||||
/// Supports read access through a ShaderResourceGroup.
|
||||
ShaderRead = AZ_BIT(2),
|
||||
ShaderRead = AZ_BIT(3),
|
||||
|
||||
/// Supports write access through ShaderResourceGroup.
|
||||
ShaderWrite = AZ_BIT(3),
|
||||
ShaderWrite = AZ_BIT(4),
|
||||
|
||||
/// Supports read-write access through a ShaderResourceGroup.
|
||||
ShaderReadWrite = ShaderRead | ShaderWrite,
|
||||
|
||||
/// Supports read access for GPU copy operations.
|
||||
CopyRead = AZ_BIT(4),
|
||||
CopyRead = AZ_BIT(5),
|
||||
|
||||
/// Supports write access for GPU copy operations.
|
||||
CopyWrite = AZ_BIT(5),
|
||||
CopyWrite = AZ_BIT(6),
|
||||
|
||||
/// Supports predication access for conditional rendering.
|
||||
Predication = AZ_BIT(6),
|
||||
Predication = AZ_BIT(7),
|
||||
|
||||
/// Supports indirect buffer access for indirect draw/dispatch.
|
||||
Indirect = AZ_BIT(7),
|
||||
Indirect = AZ_BIT(8),
|
||||
|
||||
/// Supports ray tracing acceleration structure usage.
|
||||
RayTracingAccelerationStructure = AZ_BIT(8),
|
||||
RayTracingAccelerationStructure = AZ_BIT(9),
|
||||
|
||||
/// Supports ray tracing shader table usage.
|
||||
RayTracingShaderTable = AZ_BIT(9)
|
||||
RayTracingShaderTable = AZ_BIT(10)
|
||||
|
||||
};
|
||||
|
||||
AZ_DEFINE_ENUM_BITWISE_OPERATORS(AZ::RHI::BufferBindFlags);
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace AZ
|
||||
if (SerializeContext* serializeContext = azrtti_cast<SerializeContext*>(context))
|
||||
{
|
||||
serializeContext->Class<ReflectSystemComponent, AZ::Component>()
|
||||
->Version(2);
|
||||
->Version(3);
|
||||
}
|
||||
|
||||
ReflectNamedEnums(context);
|
||||
@@ -266,6 +266,7 @@ namespace AZ
|
||||
serializeContext->Enum<BufferBindFlags>()
|
||||
->Value("None", BufferBindFlags::None)
|
||||
->Value("InputAssembly", BufferBindFlags::InputAssembly)
|
||||
->Value("DynamicInputAssembly", BufferBindFlags::DynamicInputAssembly)
|
||||
->Value("Constant", BufferBindFlags::Constant)
|
||||
->Value("CopyRead", BufferBindFlags::CopyRead)
|
||||
->Value("CopyWrite", BufferBindFlags::CopyWrite)
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace AZ
|
||||
// needs to be a multiple of elementsize as well as divisible by DX12::Alignment types.
|
||||
m_usePageAllocator = false;
|
||||
|
||||
if (!RHI::CheckBitsAny(descriptor.m_bindFlags, RHI::BufferBindFlags::ShaderWrite | RHI::BufferBindFlags::CopyWrite | RHI::BufferBindFlags::InputAssembly))
|
||||
if (!RHI::CheckBitsAny(descriptor.m_bindFlags, RHI::BufferBindFlags::ShaderWrite | RHI::BufferBindFlags::CopyWrite | RHI::BufferBindFlags::InputAssembly | RHI::BufferBindFlags::DynamicInputAssembly))
|
||||
{
|
||||
m_usePageAllocator = true;
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace AZ
|
||||
{
|
||||
m_device = &device;
|
||||
|
||||
if (RHI::CheckBitsAll(descriptor.m_bindFlags, RHI::BufferBindFlags::InputAssembly))
|
||||
if(RHI::CheckBitsAny(descriptor.m_bindFlags, RHI::BufferBindFlags::InputAssembly | RHI::BufferBindFlags::DynamicInputAssembly))
|
||||
{
|
||||
m_readOnlyState |= D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER | D3D12_RESOURCE_STATE_INDEX_BUFFER;
|
||||
}
|
||||
|
||||
@@ -670,7 +670,13 @@ namespace AZ
|
||||
}
|
||||
else
|
||||
{
|
||||
result &= AddExistingResourceEntry("texture", resourceStartPos, regId, argBufferStr);
|
||||
bool isAdditionSuccessfull = AddExistingResourceEntry("texture", resourceStartPos, regId, argBufferStr);
|
||||
if(!isAdditionSuccessfull)
|
||||
{
|
||||
//In metal depth textures use keyword depth2d/depth2d_array/depthcube/depthcube_array/depth2d_ms/depth2d_ms_array
|
||||
isAdditionSuccessfull |= AddExistingResourceEntry("depth", resourceStartPos, regId, argBufferStr);
|
||||
}
|
||||
result &= isAdditionSuccessfull;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -827,10 +833,13 @@ namespace AZ
|
||||
AZStd::string& argBufferStr) const
|
||||
{
|
||||
size_t prevEndOfLine = argBufferStr.rfind("\n", resourceStartPos);
|
||||
size_t nextEndOfLine = argBufferStr.find("\n", resourceStartPos);
|
||||
size_t startOfEntryPos = argBufferStr.find(resourceStr, prevEndOfLine);
|
||||
if(startOfEntryPos == AZStd::string::npos)
|
||||
|
||||
//Check to see if a valid entry is found.
|
||||
if(startOfEntryPos == AZStd::string::npos || startOfEntryPos > nextEndOfLine)
|
||||
{
|
||||
AZ_Error(MetalShaderPlatformName, false, "Entry-> %s not found within Descriptor set %s", resourceStr, argBufferStr.c_str());
|
||||
AZ_Error(MetalShaderPlatformName, startOfEntryPos != AZStd::string::npos, "Entry-> %s not found within Descriptor set %s", resourceStr, argBufferStr.c_str());
|
||||
return false;
|
||||
}
|
||||
else
|
||||
|
||||
@@ -295,7 +295,7 @@ namespace AZ
|
||||
|
||||
const RHI::Size sourceSize = RHI::Size(subresourceLayout.m_size.m_width, heightToCopy, 1);
|
||||
const RHI::Origin sourceOrigin = RHI::Origin(0, destHeight, depth);
|
||||
CopyBufferToImage(framePacket, image, stagingRowPitch, stagingSlicePitch,
|
||||
CopyBufferToImage(framePacket, image, stagingRowPitch, bytesCopied,
|
||||
curMip, arraySlice, sourceSize, sourceOrigin);
|
||||
|
||||
framePacket->m_dataOffset += stagingSize;
|
||||
|
||||
@@ -210,6 +210,12 @@ namespace AZ
|
||||
{
|
||||
return GetCPUGPUMemoryMode();
|
||||
}
|
||||
|
||||
//This flag is used for IA buffers that is updated frequently and hence shared mmory is the best fit
|
||||
if (RHI::CheckBitsAll(descriptor.m_bindFlags, RHI::BufferBindFlags::DynamicInputAssembly))
|
||||
{
|
||||
return MTLStorageModeShared;
|
||||
}
|
||||
|
||||
return GetCPUGPUMemoryMode();
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ namespace AZ
|
||||
bool forceUnique = RHI::CheckBitsAny(
|
||||
bufferDescriptor.m_bindFlags,
|
||||
RHI::BufferBindFlags::InputAssembly |
|
||||
RHI::BufferBindFlags::DynamicInputAssembly |
|
||||
RHI::BufferBindFlags::RayTracingAccelerationStructure |
|
||||
RHI::BufferBindFlags::RayTracingShaderTable);
|
||||
|
||||
|
||||
@@ -685,7 +685,7 @@ namespace AZ
|
||||
using BindFlags = RHI::BufferBindFlags;
|
||||
VkBufferUsageFlags usageFlags{ 0 };
|
||||
|
||||
if (RHI::CheckBitsAny(bindFlags, BindFlags::InputAssembly))
|
||||
if (RHI::CheckBitsAny(bindFlags, BindFlags::InputAssembly | BindFlags::DynamicInputAssembly))
|
||||
{
|
||||
usageFlags |=
|
||||
VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
|
||||
@@ -932,7 +932,7 @@ namespace AZ
|
||||
VkPipelineStageFlags GetResourcePipelineStateFlags(const RHI::BufferBindFlags& bindFlags)
|
||||
{
|
||||
VkPipelineStageFlags stagesFlags = {};
|
||||
if (RHI::CheckBitsAny(bindFlags, RHI::BufferBindFlags::InputAssembly))
|
||||
if (RHI::CheckBitsAny(bindFlags, RHI::BufferBindFlags::InputAssembly | RHI::BufferBindFlags::DynamicInputAssembly))
|
||||
{
|
||||
stagesFlags |= VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT;
|
||||
}
|
||||
@@ -1042,7 +1042,7 @@ namespace AZ
|
||||
VkAccessFlags GetResourceAccessFlags(const RHI::BufferBindFlags& bindFlags)
|
||||
{
|
||||
VkAccessFlags accessFlags = {};
|
||||
if (RHI::CheckBitsAny(bindFlags, RHI::BufferBindFlags::InputAssembly))
|
||||
if (RHI::CheckBitsAny(bindFlags, RHI::BufferBindFlags::InputAssembly | RHI::BufferBindFlags::DynamicInputAssembly))
|
||||
{
|
||||
accessFlags |= VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_INDEX_READ_BIT;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"BudgetInBytes": 25165824,
|
||||
"BufferPoolHeapMemoryLevel": "Host",
|
||||
"BufferPoolhostMemoryAccess": "Write",
|
||||
"BufferPoolBindFlags": "InputAssembly"
|
||||
"BufferPoolBindFlags": "DynamicInputAssembly"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace AZ
|
||||
//! buffer->Write(data, size);
|
||||
//! // Use the buffer view for DrawItem or etc.
|
||||
//! }
|
||||
//! Note: DynamicBuffer should only be used for InputAssembly buffer or Constant buffer (not supported yet).
|
||||
//! Note: DynamicBuffer should only be used for DynamicInputAssembly buffer or Constant buffer (not supported yet).
|
||||
class DynamicBuffer
|
||||
: public AZStd::intrusive_base
|
||||
{
|
||||
|
||||
@@ -74,8 +74,9 @@ namespace AZ
|
||||
|
||||
const RHI::BufferView* Buffer::GetBufferView() const
|
||||
{
|
||||
if (m_rhiBuffer->GetDescriptor().m_bindFlags == RHI::BufferBindFlags::InputAssembly)
|
||||
if(RHI::CheckBitsAny(m_rhiBuffer->GetDescriptor().m_bindFlags, RHI::BufferBindFlags::InputAssembly | RHI::BufferBindFlags::DynamicInputAssembly))
|
||||
{
|
||||
|
||||
AZ_Assert(false, "Input assembly buffer doesn't need a regular buffer view, it requires a stream or index buffer view.");
|
||||
return nullptr;
|
||||
}
|
||||
@@ -203,11 +204,11 @@ namespace AZ
|
||||
void Buffer::InitBufferView()
|
||||
{
|
||||
// Skip buffer view creation for input assembly buffers
|
||||
if (m_rhiBuffer->GetDescriptor().m_bindFlags == RHI::BufferBindFlags::InputAssembly)
|
||||
if(RHI::CheckBitsAny(m_rhiBuffer->GetDescriptor().m_bindFlags, RHI::BufferBindFlags::InputAssembly | RHI::BufferBindFlags::DynamicInputAssembly))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
m_bufferView = m_rhiBuffer->GetBufferView(m_bufferViewDescriptor);
|
||||
|
||||
if(!m_bufferView.get())
|
||||
|
||||
@@ -105,7 +105,7 @@ namespace AZ
|
||||
bufferPoolDesc.m_hostMemoryAccess = RHI::HostMemoryAccess::Write;
|
||||
break;
|
||||
case CommonBufferPoolType::DynamicInputAssembly:
|
||||
bufferPoolDesc.m_bindFlags = RHI::BufferBindFlags::InputAssembly;
|
||||
bufferPoolDesc.m_bindFlags = RHI::BufferBindFlags::DynamicInputAssembly;
|
||||
bufferPoolDesc.m_heapMemoryLevel = RHI::HeapMemoryLevel::Host;
|
||||
bufferPoolDesc.m_hostMemoryAccess = RHI::HostMemoryAccess::Write;
|
||||
break;
|
||||
|
||||
@@ -63,6 +63,7 @@ namespace AZ
|
||||
// [GFX TODO][ATOM-13182] Add unit tests for DynamicBufferAllocator's Allocate function
|
||||
RHI::Ptr<DynamicBuffer> DynamicBufferAllocator::Allocate(uint32_t size, [[maybe_unused]]uint32_t alignment)
|
||||
{
|
||||
size = RHI::AlignUp(size, alignment);
|
||||
uint32_t allocatePosition = 0;
|
||||
|
||||
//m_ringBufferStartAddress can be null for Null back end
|
||||
|
||||
@@ -174,7 +174,7 @@ namespace AZ
|
||||
}
|
||||
else if (GetAttachmentType() == RHI::AttachmentType::Buffer)
|
||||
{
|
||||
bool isInputAssembly = RHI::CheckBitsAny(m_descriptor.m_buffer.m_bindFlags, RHI::BufferBindFlags::InputAssembly);
|
||||
bool isInputAssembly = RHI::CheckBitsAny(m_descriptor.m_buffer.m_bindFlags, RHI::BufferBindFlags::InputAssembly | RHI::BufferBindFlags::DynamicInputAssembly);
|
||||
bool isConstant = RHI::CheckBitsAny(m_descriptor.m_buffer.m_bindFlags, RHI::BufferBindFlags::Constant);
|
||||
|
||||
// Since InputAssembly and Constant cannot be inferred they are set manually. If those flags are set we don't want to add inferred flags on top as it may have a performance penalty
|
||||
|
||||
@@ -173,6 +173,7 @@ namespace AZ
|
||||
m_serviceThread.join();
|
||||
Data::AssetBus::MultiHandler::BusDisconnect();
|
||||
|
||||
m_newShaderVariantPendingRequests.clear();
|
||||
m_shaderVariantTreePendingRequests.clear();
|
||||
m_shaderVariantPendingRequests.clear();
|
||||
m_shaderVariantData.clear();
|
||||
|
||||
@@ -98,15 +98,16 @@ namespace Camera
|
||||
AZ_Assert(m_atomCamera, "Attempted to activate Atom camera before component activation");
|
||||
|
||||
const AZ::Name contextName = atomViewportRequests->GetDefaultViewportContextName();
|
||||
atomViewportRequests->PushView(contextName, m_atomCamera);
|
||||
AZ::RPI::ViewportContextNotificationBus::Handler::BusConnect(contextName);
|
||||
|
||||
// Ensure the Atom camera is updated with our current transform state
|
||||
AZ::Transform localTransform;
|
||||
AZ::TransformBus::EventResult(localTransform, m_entityId, &AZ::TransformBus::Events::GetLocalTM);
|
||||
AZ::Transform worldTransform;
|
||||
AZ::TransformBus::EventResult(worldTransform, m_entityId, &AZ::TransformBus::Events::GetWorldTM);
|
||||
OnTransformChanged(localTransform, worldTransform);
|
||||
|
||||
// Push the Atom camera after we make sure we're up-to-date with our component's transform to ensure the viewport reads the correct state
|
||||
atomViewportRequests->PushView(contextName, m_atomCamera);
|
||||
AZ::RPI::ViewportContextNotificationBus::Handler::BusConnect(contextName);
|
||||
UpdateCamera();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,24 +145,36 @@ namespace SurfaceData
|
||||
|
||||
void EditorSurfaceDataSystemComponent::OnCatalogLoaded(const char* /*catalogFile*/)
|
||||
{
|
||||
//automatically register all surface tag list assets
|
||||
//automatically register all existing surface tag list assets at Editor startup
|
||||
|
||||
// First run through all the assets and trigger loads on them.
|
||||
AZStd::vector<AZ::Data::AssetId> surfaceTagAssetIds;
|
||||
|
||||
// First run through all the assets and gather up the asset IDs for all surface tag list assets
|
||||
AZ::Data::AssetCatalogRequestBus::Broadcast(&AZ::Data::AssetCatalogRequestBus::Events::EnumerateAssets,
|
||||
nullptr,
|
||||
[this](const AZ::Data::AssetId assetId, const AZ::Data::AssetInfo& assetInfo) {
|
||||
[&surfaceTagAssetIds](const AZ::Data::AssetId assetId, const AZ::Data::AssetInfo& assetInfo) {
|
||||
const auto assetType = azrtti_typeid<EditorSurfaceTagListAsset>();
|
||||
if (assetInfo.m_assetType == assetType)
|
||||
{
|
||||
m_surfaceTagNameAssets[assetId] = AZ::Data::AssetManager::Instance().GetAsset(assetId, assetType, AZ::Data::AssetLoadBehavior::Default);
|
||||
surfaceTagAssetIds.emplace_back(assetId);
|
||||
}
|
||||
},
|
||||
nullptr);
|
||||
|
||||
// After all the loads are triggered, block to make sure they've all completed.
|
||||
for (auto& asset : m_surfaceTagNameAssets)
|
||||
// Next, trigger all the loads. This is done outside of EnumerateAssets to ensure that we don't have any deadlocks caused by
|
||||
// lock inversion. If this thread locks AssetCatalogRequestBus mutex with EnumerateAssets, then locks m_assetMutex in
|
||||
// AssetManager::FindOrCreateAsset, it's possible for those locks to get locked in reverse on a loading thread, causing a deadlock.
|
||||
for (auto& assetId : surfaceTagAssetIds)
|
||||
{
|
||||
asset.second.BlockUntilLoadComplete();
|
||||
m_surfaceTagNameAssets[assetId] = AZ::Data::AssetManager::Instance().GetAsset(
|
||||
assetId, azrtti_typeid<EditorSurfaceTagListAsset>(), AZ::Data::AssetLoadBehavior::Default);
|
||||
|
||||
// If any assets are still loading (which they likely will be), listen for the OnAssetReady event and refresh the Editor
|
||||
// UI as each one finishes loading.
|
||||
if (!m_surfaceTagNameAssets[assetId].IsReady())
|
||||
{
|
||||
AZ::Data::AssetBus::MultiHandler::BusConnect(assetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ else()
|
||||
file(READ "${CMAKE_CURRENT_LIST_DIR}/project.json" project_json)
|
||||
|
||||
string(JSON project_target_name ERROR_VARIABLE json_error GET ${project_json} "project_name")
|
||||
if(${json_error})
|
||||
if(json_error)
|
||||
message(FATAL_ERROR "Unable to read key 'project_name' from 'project.json'")
|
||||
endif()
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
// Please read README.md for an explanation on why scenesrg.srgi and viewsrg.srgi are
|
||||
// located in this folder (And how you can optionally customize your own scenesrg.srgi
|
||||
// and viewsrg.srgi in your game project).
|
||||
|
||||
#include <Atom/Features/SrgSemantics.azsli>
|
||||
|
||||
partial ShaderResourceGroup RayTracingSceneSrg : SRG_RayTracingScene
|
||||
{
|
||||
/* Intentionally Empty. Helps define the SrgSemantic for RayTracingSceneSrg once.*/
|
||||
};
|
||||
|
||||
#define AZ_COLLECTING_PARTIAL_SRGS
|
||||
#include <Atom/Feature/Common/Assets/ShaderResourceGroups/RayTracingSceneSrgAll.azsli>
|
||||
#undef AZ_COLLECTING_PARTIAL_SRGS
|
||||
@@ -1,193 +0,0 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
from Params import Params
|
||||
from utils.util import *
|
||||
|
||||
|
||||
class PackageEnv(Params):
|
||||
def __init__(self, target_platform, json_file):
|
||||
super(PackageEnv, self).__init__()
|
||||
self.__cur_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(json_file, 'r') as source:
|
||||
self.__data = json.load(source)
|
||||
self.__platforms = self.__data.get('platforms')
|
||||
if target_platform not in self.__platforms:
|
||||
ly_build_error('Target platform {} is not supported'.format(target_platform))
|
||||
self.__target_platform = target_platform
|
||||
|
||||
# visited_platform is used to track the platform reference chain, in order to avoid chain cycle.
|
||||
visited_platform = [target_platform]
|
||||
platform_env = self.__platforms.get(target_platform)
|
||||
# If platform_env starts with @, it means that platform_env references another platform
|
||||
while isinstance(platform_env, str) and platform_env.startswith('@'):
|
||||
referenced_platform = platform_env.lstrip('@')
|
||||
if referenced_platform in visited_platform:
|
||||
ly_build_error('Found reference chain cycle started from {}.\nSee {}'.format(referenced_platform, json_file))
|
||||
visited_platform.append(referenced_platform)
|
||||
platform_env = self.__platforms.get(referenced_platform)
|
||||
|
||||
self.__platform_env = platform_env
|
||||
self.__global_env = self.__data.get('global')
|
||||
|
||||
def get_target_platform(self):
|
||||
return self.__target_platform
|
||||
|
||||
def get_global_env(self):
|
||||
return self.__global_env
|
||||
|
||||
def get_platform_env(self):
|
||||
return self.__platform_env
|
||||
|
||||
def __get_global_value(self, key):
|
||||
key = key.upper()
|
||||
value = self.__global_env.get(key)
|
||||
if value is None:
|
||||
ly_build_error('{} is not defined in global env'.format(key))
|
||||
return value
|
||||
|
||||
def __get_platform_value(self, key):
|
||||
key = key.upper()
|
||||
value = self.__platform_env.get(key)
|
||||
if value is None:
|
||||
ly_build_error('{} is not defined in platform env for {}'.format(key, self.__target_platform))
|
||||
return value
|
||||
|
||||
def __evaluate_boolean(self, v):
|
||||
return str(v).lower() in ['1', 'true']
|
||||
|
||||
def __get_engine_root(self):
|
||||
def validate_engine_root(engine_root):
|
||||
if not os.path.isdir(engine_root):
|
||||
return False
|
||||
return os.path.exists(os.path.join(engine_root, 'engine.json'))
|
||||
|
||||
# Jenkins only
|
||||
workspace = os.getenv('WORKSPACE')
|
||||
if workspace is not None:
|
||||
print('Environment variable WORKSPACE={} detected'.format(workspace))
|
||||
if validate_engine_root(workspace):
|
||||
print('Setting ENGINE_ROOT to {}'.format(workspace))
|
||||
return workspace
|
||||
engine_root = os.path.join(workspace, 'dev')
|
||||
if validate_engine_root(engine_root):
|
||||
print('Setting ENGINE_ROOT to {}'.format(engine_root))
|
||||
return engine_root
|
||||
print('Cannot locate ENGINE_ROOT with Environment variable WORKSPACE')
|
||||
# End Jenkins only
|
||||
|
||||
engine_root = os.getenv('ENGINE_ROOT', '')
|
||||
if validate_engine_root(engine_root):
|
||||
return engine_root
|
||||
|
||||
print('Environment variable ENGINE_ROOT is not set or invalid, checking ENGINE_ROOT in env json file')
|
||||
engine_root = self.__global_env.get('ENGINE_ROOT')
|
||||
if validate_engine_root(engine_root):
|
||||
return engine_root
|
||||
|
||||
# Set engine_root based on script location
|
||||
engine_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(self.__cur_dir))))
|
||||
print('ENGINE_ROOT from env json file is invalid, defaulting to {}'.format(engine_root))
|
||||
if validate_engine_root(engine_root):
|
||||
return engine_root
|
||||
else:
|
||||
error('Cannot Locate ENGINE_ROOT')
|
||||
|
||||
def __get_thirdparty_home(self):
|
||||
third_party_home = os.getenv('ENV_3RDPARTY_PATH', '')
|
||||
if os.path.exists(third_party_home):
|
||||
print('ENV_3RDPARTY_PATH found, using {} as 3rdParty path.'.format(third_party_home))
|
||||
return third_party_home
|
||||
third_party_home = self.__get_global_value('THIRDPARTY_HOME')
|
||||
if os.path.isdir(third_party_home):
|
||||
return third_party_home
|
||||
|
||||
# Set engine_root based on script location
|
||||
print('THIRDPARTY_HOME is not valid, looking for THIRD_PARTY_HOME')
|
||||
|
||||
# Finding THIRD_PARTY_HOME
|
||||
cur_dir = self.__get_engine_root()
|
||||
last_dir = None
|
||||
while last_dir != cur_dir:
|
||||
third_party_home = os.path.join(cur_dir, '3rdParty')
|
||||
print('Cheking THIRDPARTY_HOME {}'.format(third_party_home))
|
||||
if os.path.exists(os.path.join(third_party_home, '3rdParty.txt')):
|
||||
print('Setting THIRDPARTY_HOME to {}'.format(third_party_home))
|
||||
return third_party_home
|
||||
last_dir = cur_dir
|
||||
cur_dir = os.path.dirname(cur_dir)
|
||||
error('Cannot locate THIRDPARTY_HOME')
|
||||
|
||||
def __get_package_name_pattern(self):
|
||||
package_name_pattern = self.__get_global_value('PACKAGE_NAME_PATTERN')
|
||||
if os.getenv('PACKAGE_NAME_PATTERN') is not None:
|
||||
package_name_pattern = os.getenv('PACKAGE_NAME_PATTERN')
|
||||
return package_name_pattern
|
||||
|
||||
def __get_build_number(self):
|
||||
build_number = self.__get_global_value('BUILD_NUMBER')
|
||||
if os.getenv('BUILD_NUMBER') is not None:
|
||||
build_number = os.getenv('BUILD_NUMBER')
|
||||
return build_number
|
||||
|
||||
def __get_p4_changelist(self):
|
||||
p4_changelist = self.__get_global_value('P4_CHANGELIST')
|
||||
if os.getenv('P4_CHANGELIST') is not None:
|
||||
p4_changelist = os.getenv('P4_CHANGELIST')
|
||||
return p4_changelist
|
||||
|
||||
def __get_major_version(self):
|
||||
major_version = self.__get_global_value('MAJOR_VERSION')
|
||||
if os.getenv('MAJOR_VERSION') is not None:
|
||||
major_version = os.getenv('MAJOR_VERSION')
|
||||
return major_version
|
||||
|
||||
def __get_minor_version(self):
|
||||
minor_version = self.__get_global_value('MINOR_VERSION')
|
||||
if os.getenv('MINOR_VERSION') is not None:
|
||||
minor_version = os.getenv('MINOR_VERSION')
|
||||
return minor_version
|
||||
|
||||
def __get_scrub_params(self):
|
||||
return self.__get_platform_value('SCRUB_PARAMS')
|
||||
|
||||
def __get_validator_platforms(self):
|
||||
return self.__get_platform_value('VALIDATOR_PLATFORMS')
|
||||
|
||||
def __get_package_targets(self):
|
||||
return self.__get_platform_value('PACKAGE_TARGETS')
|
||||
|
||||
def __get_build_targets(self):
|
||||
return self.__get_platform_value('BUILD_TARGETS')
|
||||
|
||||
def __get_asset_processor_path(self):
|
||||
return self.__get_platform_value('ASSET_PROCESSOR_PATH')
|
||||
|
||||
def __get_asset_game_folders(self):
|
||||
return self.__get_platform_value('ASSET_GAME_FOLDERS')
|
||||
|
||||
def __get_asset_platform(self):
|
||||
return self.__get_platform_value('ASSET_PLATFORM')
|
||||
|
||||
def __get_bootstrap_cfg_game_folder(self):
|
||||
return self.__get_platform_value('BOOTSTRAP_CFG_GAME_FOLDER')
|
||||
|
||||
def __get_run_launcher_unit_test(self):
|
||||
run_launcher_unit_test = os.getenv('RUN_LAUNCHER_UNIT_TEST')
|
||||
if run_launcher_unit_test is None:
|
||||
run_launcher_unit_test = self.__platform_env.get('RUN_LAUNCHER_UNIT_TEST')
|
||||
return self.__evaluate_boolean(run_launcher_unit_test)
|
||||
|
||||
def __get_skip_build(self):
|
||||
skip_build = os.getenv('SKIP_BUILD')
|
||||
if skip_build is None:
|
||||
skip_build = self.__platform_env.get('SKIP_BUILD')
|
||||
return self.__evaluate_boolean(skip_build)
|
||||
@@ -1,81 +0,0 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
from utils.util import *
|
||||
|
||||
|
||||
class Params(object):
|
||||
def __init__(self):
|
||||
# Cache params
|
||||
self.__params = {}
|
||||
|
||||
def get(self, param_name):
|
||||
param_value = self.__params.get(param_name)
|
||||
if param_value is not None:
|
||||
return param_value
|
||||
# Call __get_${param_name} function
|
||||
func = getattr(self, '_{}__get_{}'.format(self.__class__.__name__, param_name.lower()), None)
|
||||
if func is not None:
|
||||
param_value = func()
|
||||
# Replace all ${env} in value
|
||||
if isinstance(param_value, str):
|
||||
param_value = self.__process_string(param_name, param_value)
|
||||
elif isinstance(param_value, list):
|
||||
param_value = self.__process_list(param_name, param_value)
|
||||
elif isinstance(param_value, dict):
|
||||
param_value = self.__process_dict(param_name, param_value)
|
||||
# Cache param
|
||||
self.__params[param_name] = param_value
|
||||
return param_value
|
||||
ly_build_error('method __get_{} is not defined in class {}'.format(param_name.lower(), self.__class__.__name__))
|
||||
|
||||
def set(self, param_name, param_value):
|
||||
self.__params[param_name] = param_value
|
||||
|
||||
def exists(self, param_name):
|
||||
try:
|
||||
self.get(param_name)
|
||||
except LyBuildError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __process_string(self, param_name, param_value):
|
||||
# Find all param with format ${param}
|
||||
params = re.findall('\${(\w+)}', param_value)
|
||||
# Avoid using the same param name in value, like 'WORKSPACE': '${WORKSPACE} some string'
|
||||
if param_name in params:
|
||||
ly_build_error('The use of same parameter name({}) in value is not allowed'.format(param_name))
|
||||
# Replace ${param} with actual value
|
||||
for param in params:
|
||||
param_value = param_value.replace('${' + param + '}', self.get(param))
|
||||
return param_value
|
||||
|
||||
def __process_list(self, param_name, param_value):
|
||||
processed_list = []
|
||||
for entry in param_value:
|
||||
if isinstance(entry, str):
|
||||
entry = self.__process_string(param_name, entry)
|
||||
elif isinstance(entry, list):
|
||||
entry = self.__process_list(param_name, entry)
|
||||
elif isinstance(entry, dict):
|
||||
entry = self.__process_dict(param_name, entry)
|
||||
processed_list.append(entry)
|
||||
return processed_list
|
||||
|
||||
def __process_dict(self, param_name, param_value):
|
||||
for key in param_value:
|
||||
if isinstance(param_value[key], str):
|
||||
param_value[key] = self.__process_string(param_name, param_value[key])
|
||||
elif isinstance(param_value[key], list):
|
||||
param_value[key] = self.__process_list(param_name, param_value[key])
|
||||
elif isinstance(param_value[key], dict):
|
||||
param_value[key] = self.__process_dict(param_name, param_value[key])
|
||||
return param_value
|
||||
@@ -1,358 +0,0 @@
|
||||
#
|
||||
# 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 sys
|
||||
import glob_to_regex
|
||||
import zipfile
|
||||
import timeit
|
||||
import stat
|
||||
from optparse import OptionParser
|
||||
from PackageEnv import PackageEnv
|
||||
from ci_build import build
|
||||
from utils.util import *
|
||||
from utils.lib.glob3 import glob
|
||||
|
||||
|
||||
def package(options):
|
||||
package_platform = options.package_platform
|
||||
package_env = PackageEnv(package_platform, options.package_env)
|
||||
engine_root = package_env.get('ENGINE_ROOT')
|
||||
|
||||
# Ask the validator code to tell us which files need to be removed from the package
|
||||
prohibited_file_mask = get_prohibited_file_mask(package_platform, engine_root)
|
||||
|
||||
# Scrub files. This is destructive, but is necessary to allow the current file existance checks to work properly. Better to copy and then build, or to
|
||||
# mask on sync, but this is what we have for now
|
||||
# No need to run scrubbing script since all restricted platform codes are moved to dev/restricted folder
|
||||
#scrub_files(package_env, prohibited_file_mask)
|
||||
|
||||
# validate files
|
||||
validate_restricted_files(package_platform, package_env)
|
||||
|
||||
# Override values in bootstrap.cfg for PC package
|
||||
override_bootstrap_cfg(package_env)
|
||||
|
||||
# Generate GameTemplates whitelist information for metrics reporting
|
||||
template_whitelist_script = os.path.join(engine_root, 'Tools/build/JenkinsScripts/distribution/Metrics/GameTemplates/buildGameTemplateWhitelist.py')
|
||||
if os.path.exists(template_whitelist_script):
|
||||
if sys.platform == 'win32':
|
||||
python = os.path.join(engine_root, 'Tools', 'Python', 'python.cmd')
|
||||
else:
|
||||
python = os.path.join(engine_root, 'Tools', 'Python', 'python.sh')
|
||||
project_templates_folder = os.path.join(engine_root, 'ProjectTemplates')
|
||||
args = [python, template_whitelist_script, '--projectTemplatesFolder', project_templates_folder]
|
||||
#execute_system_call(args)
|
||||
|
||||
if not package_env.get('SKIP_BUILD'):
|
||||
print('SKIP_BUILD is False, running CMake build...')
|
||||
cmake_build(package_env)
|
||||
|
||||
# TODO Compile Assets
|
||||
#if package_env.exists('ASSET_PROCESSOR_PATH'):
|
||||
# compile_assets(package_env)
|
||||
|
||||
#create packages
|
||||
create_packages(package_env)
|
||||
|
||||
|
||||
def override_bootstrap_cfg(package_env):
|
||||
print('Override values in bootstrap.cfg')
|
||||
engine_root = package_env.get('ENGINE_ROOT')
|
||||
bootstrap_path = os.path.join(engine_root, 'bootstrap.cfg')
|
||||
replace_values = {'project_path':'{}'.format(package_env.get('BOOTSTRAP_CFG_GAME_FOLDER'))}
|
||||
try:
|
||||
with open(bootstrap_path, 'r') as bootstrap_cfg:
|
||||
content = bootstrap_cfg.read()
|
||||
except:
|
||||
error('Cannot read file {}'.format(bootstrap_path))
|
||||
content = content.split('\n')
|
||||
new_content = []
|
||||
for line in content:
|
||||
if not line.startswith('--'):
|
||||
strs = line.split('=')
|
||||
if len(strs):
|
||||
key = strs[0].strip(' ')
|
||||
if key in replace_values:
|
||||
line = '{}={}'.format(key, replace_values[key])
|
||||
new_content.append(line)
|
||||
try:
|
||||
with open(bootstrap_path, 'w') as out:
|
||||
out.write('\n'.join(new_content))
|
||||
except:
|
||||
error('Cannot write to file {}'.format(bootstrap_path))
|
||||
print('{} updated with value {}'.format(bootstrap_path, replace_values))
|
||||
|
||||
|
||||
def get_prohibited_file_mask(package_platform, engine_root):
|
||||
sys.path.append(os.path.join(engine_root, 'Tools', 'build', 'JenkinsScripts', 'distribution', 'scrubbing'))
|
||||
from validator_data_LEGAL_REVIEW_REQUIRED import get_prohibited_platforms_for_package
|
||||
|
||||
# The list of prohibited platforms is controlled by the validator on a per-package basis
|
||||
prohibited_platforms = get_prohibited_platforms_for_package(package_platform)
|
||||
prohibited_platforms.append('all')
|
||||
excludes_list = []
|
||||
for p in prohibited_platforms:
|
||||
platform_excludes = glob_to_regex.generate_excludes_for_platform(engine_root, p)
|
||||
excludes_list.extend(platform_excludes)
|
||||
prohibited_file_mask = re.compile('|'.join(excludes_list), re.IGNORECASE)
|
||||
return prohibited_file_mask
|
||||
|
||||
|
||||
def scrub_files(package_env, prohibited_file_mask):
|
||||
print('Perform the Code Scrubbing')
|
||||
engine_root = package_env.get('ENGINE_ROOT')
|
||||
|
||||
success = True
|
||||
for dirname, subFolders, files in os.walk(engine_root):
|
||||
for filename in files:
|
||||
full_path = os.path.join(dirname, filename)
|
||||
if prohibited_file_mask.match(full_path):
|
||||
try:
|
||||
print('Deleting: {}'.format(full_path))
|
||||
os.chmod(full_path, stat.S_IWRITE)
|
||||
os.unlink(full_path)
|
||||
except:
|
||||
e = sys.exc_info()[0]
|
||||
sys.stderr.write('Error: could not delete {} ... aborting.\n'.format(full_path))
|
||||
sys.stderr.write('{}\n'.format(str(e)))
|
||||
success = False
|
||||
if not success:
|
||||
sys.stderr.write('ERROR: scrub_files failed\n')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def validate_restricted_files(package, package_env):
|
||||
print('Perform the Code Scrubbing')
|
||||
engine_root = package_env.get('ENGINE_ROOT')
|
||||
|
||||
# Run validator
|
||||
success = True
|
||||
validator_path = os.path.join(engine_root, 'Tools/build/JenkinsScripts/distribution/scrubbing/validator.py')
|
||||
if sys.platform == 'win32':
|
||||
python = os.path.join(engine_root, 'Tools', 'Python', 'python3.cmd')
|
||||
else:
|
||||
python = os.path.join(engine_root, 'Tools', 'Python', 'python3.sh')
|
||||
args = [python, validator_path, '--package', package, engine_root]
|
||||
return_code = safe_execute_system_call(args)
|
||||
if return_code != 0:
|
||||
success = False
|
||||
if not success:
|
||||
error('Restricted file validator failed.')
|
||||
print('Restricted file validator completed successfully.')
|
||||
|
||||
|
||||
def cmake_build(package_env):
|
||||
build_targets = package_env.get('BUILD_TARGETS')
|
||||
for build_target in build_targets:
|
||||
build(build_target['BUILD_CONFIG_FILENAME'], build_target['PLATFORM'], build_target['TYPE'])
|
||||
|
||||
|
||||
def create_packages(package_env):
|
||||
package_targets = package_env.get('PACKAGE_TARGETS')
|
||||
for package_target in package_targets:
|
||||
print('Creating zipfile for package target {}'.format(package_target))
|
||||
cur_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
filelist = os.path.join(cur_dir, 'package_filelists', '{}.json'.format(package_target['TYPE']))
|
||||
with open(filelist, 'r') as source:
|
||||
data = json.load(source)
|
||||
lyengine = os.path.dirname(package_env.get('ENGINE_ROOT'))
|
||||
print('Calculating filelists...')
|
||||
files = {}
|
||||
# We have to include 3rdParty in Mac/Console packages until LAD is available for those platforms
|
||||
# Remove this when LAD is available for those platforms.
|
||||
if package_target['TYPE'] in ['cmake_consoles', 'consoles']:
|
||||
files.update(get_3rdparty_filelist(package_env, 'common'))
|
||||
files.update(get_3rdparty_filelist(package_env, 'vc141'))
|
||||
files.update(get_3rdparty_filelist(package_env, 'vc142'))
|
||||
files.update(get_3rdparty_filelist(package_env, 'provo'))
|
||||
elif package_target['TYPE'] in ['cmake_atom_pc']:
|
||||
files.update(get_3rdparty_filelist(package_env, 'common'))
|
||||
files.update(get_3rdparty_filelist(package_env, 'vc141'))
|
||||
files.update(get_3rdparty_filelist(package_env, 'vc142'))
|
||||
elif package_target['TYPE'] in ['cmake_all']:
|
||||
if package_env.get_target_platform() == 'mac':
|
||||
files.update(get_3rdparty_filelist(package_env, 'common'))
|
||||
files.update(get_3rdparty_filelist(package_env, 'mac'))
|
||||
elif package_env.get_target_platform() == 'consoles':
|
||||
files.update(get_3rdparty_filelist(package_env, 'common'))
|
||||
files.update(get_3rdparty_filelist(package_env, 'vc141'))
|
||||
files.update(get_3rdparty_filelist(package_env, 'vc142'))
|
||||
files.update(get_3rdparty_filelist(package_env, 'provo'))
|
||||
|
||||
if '@lyengine' in data:
|
||||
if '@engine_root' in data['@lyengine']:
|
||||
engine_root_basename = os.path.basename(package_env.get('ENGINE_ROOT'))
|
||||
data['@lyengine'][engine_root_basename] = data['@lyengine']['@engine_root']
|
||||
data['@lyengine'].pop('@engine_root')
|
||||
files.update(filter_files(data['@lyengine'], lyengine))
|
||||
if '@3rdParty' in data:
|
||||
files.update(filter_files(data['@3rdParty'], package_env.get('THIRDPARTY_HOME')))
|
||||
package_path = os.path.join(lyengine, package_target['PACKAGE_NAME'])
|
||||
print('Creating zipfile at {}'.format(package_path))
|
||||
start = timeit.default_timer()
|
||||
|
||||
with zipfile.ZipFile(package_path, 'w', compression=zipfile.ZIP_DEFLATED, allowZip64=True) as myzip:
|
||||
for f in files:
|
||||
if os.path.islink(f):
|
||||
zipInfo = zipfile.ZipInfo(files[f])
|
||||
zipInfo.create_system = 3
|
||||
# long type of hex val of '0xA1ED0000L',
|
||||
# say, symlink attr magic...
|
||||
#zipInfo.external_attr = 0xA1ED0000L
|
||||
zipInfo.external_attr |= 0xA0000000
|
||||
myzip.writestr(zipInfo, os.readlink(f))
|
||||
else:
|
||||
myzip.write(f, files[f])
|
||||
|
||||
stop = timeit.default_timer()
|
||||
total_time = int(stop - start)
|
||||
print('{} is created. Total time: {} seconds.'.format(package_path, total_time))
|
||||
|
||||
def get_MD5(file_path):
|
||||
from hashlib import md5
|
||||
chunk_size = 200 * 1024
|
||||
h = md5()
|
||||
with open(file_path, 'rb') as f:
|
||||
while True:
|
||||
chunk = f.read(chunk_size)
|
||||
if len(chunk):
|
||||
h.update(chunk)
|
||||
else:
|
||||
break
|
||||
return h.hexdigest()
|
||||
|
||||
md5_file = '{}.MD5'.format(package_path)
|
||||
print('Creating MD5 file at {}'.format(md5_file))
|
||||
start = timeit.default_timer()
|
||||
with open(md5_file, 'w') as output:
|
||||
output.write(get_MD5(package_path))
|
||||
stop = timeit.default_timer()
|
||||
total_time = int(stop - start)
|
||||
print('{} is created. Total time: {} seconds.'.format(md5_file, total_time))
|
||||
|
||||
|
||||
def filter_files(data, base, prefix='', support_symlinks=True):
|
||||
includes = {}
|
||||
excludes = set()
|
||||
for key, value in data.items():
|
||||
pattern = os.path.join(base, prefix, key)
|
||||
if not isinstance(value, dict):
|
||||
pattern = os.path.normpath(pattern)
|
||||
result = glob(pattern, recursive=True)
|
||||
files = [x for x in result if os.path.isfile(x) or (support_symlinks and os.path.islink(x))]
|
||||
if value == "#exclude":
|
||||
excludes.update(files)
|
||||
elif value == "#include":
|
||||
for file in files:
|
||||
includes[file] = os.path.relpath(file, base)
|
||||
else:
|
||||
if value.startswith('#move:'):
|
||||
for file in files:
|
||||
file_name = os.path.relpath(file, os.path.join(base, prefix))
|
||||
dst_dir = value.replace('#move:', '').strip(' ')
|
||||
includes[file] = os.path.join(dst_dir, file_name)
|
||||
elif value.startswith('#rename:'):
|
||||
for file in files:
|
||||
dst_file = value.replace('#rename:', '').strip(' ')
|
||||
includes[file] = dst_file
|
||||
else:
|
||||
warn('Unknown directive {} for pattern {}'.format(value, pattern))
|
||||
else:
|
||||
includes.update(filter_files(value, base, os.path.join(prefix, key), support_symlinks))
|
||||
|
||||
for exclude in excludes:
|
||||
try:
|
||||
includes.pop(exclude)
|
||||
except KeyError:
|
||||
pass
|
||||
return includes
|
||||
|
||||
|
||||
def get_3rdparty_filelist(package_env, platform, support_symlinks=True):
|
||||
engine_root = package_env.get('ENGINE_ROOT')
|
||||
include_pattern_file = 'include_pattern_file'
|
||||
if os.path.isfile(include_pattern_file):
|
||||
os.remove(include_pattern_file)
|
||||
exclude_pattern_file = 'exclude_pattern_file'
|
||||
if os.path.isfile(exclude_pattern_file):
|
||||
os.remove(exclude_pattern_file)
|
||||
versions_file = 'versions_file'
|
||||
if os.path.isfile(versions_file):
|
||||
os.remove(versions_file)
|
||||
|
||||
# Generate 3rdParty version file
|
||||
ly_dep_version_tool = os.path.join(engine_root, 'Tools/build/JenkinsScripts/distribution/ly_dep_version_tool.py')
|
||||
setup_assistant_config = os.path.join(engine_root, 'SetupAssistantConfig.json')
|
||||
if sys.platform == 'win32':
|
||||
python = os.path.join(engine_root, 'Tools', 'Python', 'python.cmd')
|
||||
else:
|
||||
python = os.path.join(engine_root, 'Tools', 'Python', 'python.sh')
|
||||
args = [python, ly_dep_version_tool, '-o', versions_file, '-s', setup_assistant_config]
|
||||
execute_system_call(args)
|
||||
|
||||
# Generate 3rdParty include pattern and exclude pattern
|
||||
generate_external_3rdparty_file_list = os.path.join(engine_root, 'Tools/build/JenkinsScripts/distribution/ThirdParty/generate_external_3rdparty_file_list.py')
|
||||
package_config = os.path.join(engine_root, 'Tools/build/JenkinsScripts/distribution/ThirdParty/CMakePackageConfig.json')
|
||||
args = [python, generate_external_3rdparty_file_list, '-s', versions_file, '-c', package_config, '-p', platform, '-i', include_pattern_file, '-e', exclude_pattern_file]
|
||||
execute_system_call(args)
|
||||
|
||||
# Calculate filelist using include pattern and exclude pattern
|
||||
thirdparty_home = package_env.get('THIRDPARTY_HOME')
|
||||
filelist = {}
|
||||
with open(include_pattern_file, 'r') as source:
|
||||
include_patterns = source.readlines()
|
||||
for include_pattern in include_patterns:
|
||||
pattern = os.path.join(thirdparty_home, include_pattern.strip('\n'))
|
||||
pattern = os.path.normpath(pattern)
|
||||
result = glob(pattern, recursive=True)
|
||||
files = [x for x in result if os.path.isfile(x) or (support_symlinks and os.path.islink(x))]
|
||||
for file in files:
|
||||
filelist[file] = os.path.join('3rdParty', os.path.relpath(file, thirdparty_home))
|
||||
|
||||
with open(exclude_pattern_file, 'r') as source:
|
||||
exclude_patterns = source.readlines()
|
||||
for exclude_pattern in exclude_patterns:
|
||||
pattern = os.path.join(thirdparty_home, exclude_pattern.strip('\n'))
|
||||
pattern = os.path.normpath(pattern)
|
||||
result = glob(pattern, recursive=True)
|
||||
files = [x for x in result if os.path.isfile(x) or (support_symlinks and os.path.islink(x))]
|
||||
for file in files:
|
||||
try:
|
||||
filelist.pop(file)
|
||||
except KeyError:
|
||||
pass
|
||||
return filelist
|
||||
|
||||
|
||||
def parse_args():
|
||||
cur_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
parser = OptionParser()
|
||||
parser.add_option("--release", dest="release", default=False, action='store_true', help="Release build")
|
||||
parser.add_option("--package_platform", dest="package_platform", default='consoles', help="Target platform to package")
|
||||
parser.add_option("--package_env", dest="package_env", default=os.path.join(cur_dir, "cmake_package_env.json"),
|
||||
help="JSON file that defines package environment variables")
|
||||
parser.add_option("--package_build_configurations_json", dest="package_build_configurations_json",
|
||||
default=os.path.join(cur_dir, "package_build_configurations.json"),
|
||||
help="JSON file that defines build parameters")
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
if options.package_platform is None:
|
||||
error('No package platform specified')
|
||||
return options, args
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
(options, args) = parse_args()
|
||||
package(options)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
{
|
||||
"global":{
|
||||
"ENGINE_ROOT":"",
|
||||
"THIRDPARTY_HOME":"",
|
||||
"PACKAGE_NAME_PATTERN":"lumberyard-${MAJOR_VERSION}.${MINOR_VERSION}-${P4_CHANGELIST}",
|
||||
"BUILD_NUMBER":"0",
|
||||
"P4_CHANGELIST":"0",
|
||||
"MAJOR_VERSION":"0",
|
||||
"MINOR_VERSION":"0",
|
||||
"LAD_PACKAGE_STORAGE_URL":"https://d7qxx8qkrwa8l.cloudfront.net"
|
||||
},
|
||||
"platforms":{
|
||||
"consoles":{
|
||||
"PACKAGE_TARGETS":[
|
||||
{
|
||||
"TYPE": "cmake_all",
|
||||
"PACKAGE_NAME": "${PACKAGE_NAME_PATTERN}-consoles-${BUILD_NUMBER}.zip"
|
||||
},
|
||||
{
|
||||
"TYPE": "symbols",
|
||||
"PACKAGE_NAME": "${PACKAGE_NAME_PATTERN}-consoles-symbols-${BUILD_NUMBER}.zip"
|
||||
}
|
||||
],
|
||||
"BOOTSTRAP_CFG_GAME_FOLDER":"AutomatedTesting",
|
||||
"SKIP_BUILD": 1,
|
||||
"BUILD_TARGETS":[
|
||||
{
|
||||
"BUILD_CONFIG_FILENAME": "build_config.json",
|
||||
"PLATFORM": "Windows",
|
||||
"TYPE": "profile_vs2017"
|
||||
},
|
||||
{
|
||||
"BUILD_CONFIG_FILENAME": "build_config.json",
|
||||
"PLATFORM": "Windows",
|
||||
"TYPE": "profile_vs2019"
|
||||
},
|
||||
{
|
||||
"BUILD_CONFIG_FILENAME": "build_config.json",
|
||||
"PLATFORM": "Provo",
|
||||
"TYPE": "profile"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cmake_atom_pc":{
|
||||
"PACKAGE_TARGETS":[
|
||||
{
|
||||
"TYPE": "cmake_atom_pc",
|
||||
"PACKAGE_NAME": "${PACKAGE_NAME_PATTERN}-cmake_atom_pc-${BUILD_NUMBER}.zip"
|
||||
}
|
||||
],
|
||||
"BOOTSTRAP_CFG_GAME_FOLDER":"AtomSampleViewer;AtomTest",
|
||||
"SKIP_BUILD": 1,
|
||||
"BUILD_TARGETS":[
|
||||
{
|
||||
"BUILD_CONFIG_FILENAME": "package_build_config.json",
|
||||
"PLATFORM": "Windows",
|
||||
"TYPE": "profile_vs2017_atom"
|
||||
},
|
||||
{
|
||||
"BUILD_CONFIG_FILENAME": "package_build_config.json",
|
||||
"PLATFORM": "Windows",
|
||||
"TYPE": "profile_vs2019_atom"
|
||||
}
|
||||
]
|
||||
},
|
||||
"mac":{
|
||||
"PACKAGE_TARGETS":[
|
||||
{
|
||||
"TYPE": "cmake_all",
|
||||
"PACKAGE_NAME": "${PACKAGE_NAME_PATTERN}-cmake_mac_all-${BUILD_NUMBER}.zip"
|
||||
}
|
||||
],
|
||||
"BOOTSTRAP_CFG_GAME_FOLDER":"AutomatedTesting",
|
||||
"SKIP_BUILD": 1,
|
||||
"BUILD_TARGETS":[
|
||||
{
|
||||
"BUILD_CONFIG_FILENAME": "build_config.json",
|
||||
"PLATFORM": "Mac",
|
||||
"TYPE": "profile"
|
||||
},
|
||||
{
|
||||
"BUILD_CONFIG_FILENAME": "build_config.json",
|
||||
"PLATFORM": "iOS",
|
||||
"TYPE": "profile"
|
||||
}
|
||||
]
|
||||
},
|
||||
"linux":{
|
||||
"PACKAGE_TARGETS":[
|
||||
{
|
||||
"TYPE": "cmake_all",
|
||||
"PACKAGE_NAME": "${PACKAGE_NAME_PATTERN}-cmake_linux_all-${BUILD_NUMBER}.zip"
|
||||
}
|
||||
],
|
||||
"BOOTSTRAP_CFG_GAME_FOLDER":"AutomatedTesting",
|
||||
"SKIP_BUILD": 1,
|
||||
"BUILD_TARGETS":[
|
||||
{
|
||||
"BUILD_CONFIG_FILENAME": "build_config.json",
|
||||
"PLATFORM": "Linux",
|
||||
"TYPE": "profile"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
"""
|
||||
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.
|
||||
|
||||
Downloads the latest package from a S3 and unzips it to a desired location.
|
||||
"""
|
||||
import argparse
|
||||
import boto3
|
||||
import os
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
|
||||
def download_and_unzip_package(bucket_name, package_regex, build_number_regex, folder_path, destination_path):
|
||||
"""
|
||||
Downloads a given package from a S3 and unzips it.
|
||||
:param bucket_name: S3 bucket
|
||||
:param package_regex: Regex to find the desired package
|
||||
:param build_number_regex: Regex to find the build number from the package name
|
||||
:param folder_path: Folder path to the package
|
||||
:param destination_path: Where to download the package to
|
||||
:return:
|
||||
"""
|
||||
# Make sure the directory exists
|
||||
if not os.path.isdir(destination_path):
|
||||
os.makedirs(destination_path)
|
||||
|
||||
# Sorting function for latest package
|
||||
def get_build_number(file_name_to_parse):
|
||||
return re.search(build_number_regex, file_name_to_parse).group(0)[:-4] # [:-4] removes the .zip extension
|
||||
|
||||
s3 = boto3.resource('s3')
|
||||
bucket = s3.Bucket(bucket_name)
|
||||
largest_build_number = -1
|
||||
latest_file = 'No file found!'
|
||||
# Find the latest package
|
||||
print 'Reading files from bucket...'
|
||||
for bucket_file in bucket.objects.filter(Prefix=folder_path):
|
||||
file_name = bucket_file.key
|
||||
if re.search(package_regex, file_name) and get_build_number(file_name) > largest_build_number:
|
||||
largest_build_number = get_build_number(file_name)
|
||||
latest_file = file_name
|
||||
|
||||
package_name = latest_file.split('/')[-1]
|
||||
|
||||
# Download the package
|
||||
print('Downloading package: {0} from bucket {1} to {2}'.format(latest_file, bucket_name, destination_path))
|
||||
s3.Bucket(bucket_name).download_file(latest_file, os.path.join(destination_path, package_name))
|
||||
|
||||
# Unzip the package
|
||||
with zipfile.ZipFile(os.path.join(destination_path, package_name), 'r') as zip_ref:
|
||||
print('Unzipping package: {0} to {1}'.format(package_name, destination_path))
|
||||
zip_ref.extractall(destination_path)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-b', '--bucket_name', required=True, help='Bucket that holds the package.')
|
||||
parser.add_argument('-p', '--package_regex', required=True,
|
||||
help='Regex to identify a package. Such as: lumberyard-0.0-[\d]{6,7}-pc-[\d]{4}.zip\s to find '
|
||||
'the main pc package.')
|
||||
parser.add_argument('-n', '--build_number_regex', required=True,
|
||||
help='Regex to identify the build number. Such as [\d]{4,5}.zip$ to find the build number from '
|
||||
'the name of the main pc package')
|
||||
parser.add_argument('-d', '--destination_path', required=True, help='Destination for the contents of the packages.')
|
||||
parser.add_argument('-f', '--folder_path', help='Folder that contains the package, must include /.')
|
||||
|
||||
args = parser.parse_args()
|
||||
download_and_unzip_package(args.bucket_name, args.package_regex, args.build_number_regex, args.folder_path,
|
||||
args.destination_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,55 +0,0 @@
|
||||
"""
|
||||
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.
|
||||
|
||||
Downloads packages and unzips them.
|
||||
"""
|
||||
import argparse
|
||||
import boto3
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
|
||||
def download_and_unzip_packages(bucket_name, package_key, folder_path, destination_path):
|
||||
"""
|
||||
Downloads a given package from a S3 and unzips it.
|
||||
:param bucket_name: S3 bucket
|
||||
:param package_key: Key for the package
|
||||
:param folder_path: Folder path to the package
|
||||
:param destination_path: Where to download the package to
|
||||
:return:
|
||||
"""
|
||||
# Make sure the directory exists
|
||||
if not os.path.isdir(destination_path):
|
||||
os.makedirs(destination_path)
|
||||
|
||||
# Download the package
|
||||
s3 = boto3.resource('s3')
|
||||
print('Downloading package: {0} from bucket {1} to {2}'.format(package_key, bucket_name, destination_path))
|
||||
s3.Bucket(bucket_name).download_file(folder_path + package_key, os.path.join(destination_path, package_key))
|
||||
|
||||
# Unzip the package
|
||||
with zipfile.ZipFile(os.path.join(destination_path, package_key), 'r') as zip_ref:
|
||||
print('Unzipping package: {0} to {1}'.format(package_key, destination_path))
|
||||
zip_ref.extractall(destination_path)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-b', '--bucket_name', required=True, help='Bucket that holds the package.')
|
||||
parser.add_argument('-p', '--package_key', required=True, help='Desired package\'s key.')
|
||||
parser.add_argument('-d', '--destination_path', required=True, help='Destination for the contents of the packages.')
|
||||
parser.add_argument('-f', '--folder_path', help='Folder that contains the package, must include /.')
|
||||
|
||||
args = parser.parse_args()
|
||||
download_and_unzip_packages(args.bucket_name, args.package_key, args.folder_path, args.destination_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,57 +0,0 @@
|
||||
"""
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
'''
|
||||
All this script is doing is writing grabbing a file that was written previously marking the start of when the Perforce would run
|
||||
and then getting the current time to find out how long we spent in Perforce
|
||||
'''
|
||||
|
||||
import time
|
||||
|
||||
from utils.util import *
|
||||
|
||||
|
||||
def write_metrics():
|
||||
enable_build_metrics = os.environ.get('ENABLE_BUILD_METRICS')
|
||||
metrics_namespace = os.environ.get('METRICS_NAMESPACE')
|
||||
if enable_build_metrics == 'true':
|
||||
scm_end = int(time.time())
|
||||
workspace = os.environ.get('WORKSPACE')
|
||||
metrics_file_name = 'scm_start.txt'
|
||||
if workspace is None:
|
||||
safe_exit_with_error('{} must be run in Jenkins job.'.format(os.path.basename(__file__)))
|
||||
try:
|
||||
with open(os.path.join(workspace, metrics_file_name), 'r') as f:
|
||||
scm_start = int(f.readline())
|
||||
except:
|
||||
safe_exit_with_error('Failed to read from {}'.format(metrics_file_name))
|
||||
|
||||
scm_total = scm_end - scm_start
|
||||
|
||||
script_path = os.path.join(workspace, 'dev/Tools/build/waf-1.7.13/build_metrics/write_build_metric.py')
|
||||
|
||||
build_tag = os.environ.get('BUILD_TAG')
|
||||
p4_changelist = os.environ.get('P4_CHANGELIST')
|
||||
|
||||
if build_tag is not None and p4_changelist is not None:
|
||||
os.environ['BUILD_ID'] = '{0}.{1}'.format(build_tag, p4_changelist)
|
||||
|
||||
cwd = os.getcwd()
|
||||
os.chdir(os.path.join(workspace, 'dev'))
|
||||
cmd = 'python {} SCMTime {} Seconds --enable-build-metrics {} --metrics-namespace {} --project-spec None'.format(script_path, scm_total, True, metrics_namespace)
|
||||
# metrics call shouldn't fail the job
|
||||
safe_execute_system_call(cmd, shell=True)
|
||||
os.chdir(cwd)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
write_metrics()
|
||||
@@ -1,12 +0,0 @@
|
||||
"""
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (c) Amazon.com, Inc.
|
||||
-->
|
||||
<project name="CopyLadThirdParty" default="CopyLadThirdParty" basedir="../../../">
|
||||
<fail message="Error: 3rdParty.home is not set">
|
||||
<condition>
|
||||
<not>
|
||||
<isset property="3rdParty.home"/>
|
||||
</not>
|
||||
</condition>
|
||||
</fail>
|
||||
<fail message="Error: 3rdParty.destination is not set">
|
||||
<condition>
|
||||
<not>
|
||||
<isset property="3rdParty.destination"/>
|
||||
</not>
|
||||
</condition>
|
||||
</fail>
|
||||
<fail message="Error: platform is not set">
|
||||
<condition>
|
||||
<not>
|
||||
<isset property="platform"/>
|
||||
</not>
|
||||
</condition>
|
||||
</fail>
|
||||
|
||||
<include file="../../distribution/package/3rdParty.xml" optional="false" />
|
||||
<target name="CopyLadThirdParty">
|
||||
<ThirdPartySDKsGeneratePlatformPatternSet platform="${platform}"/>
|
||||
<copy todir="${3rdParty.destination}">
|
||||
<fileset dir="${3rdParty.home}">
|
||||
<patternset refid="include-3rdparty-patternset-common" />
|
||||
</fileset>
|
||||
</copy>
|
||||
<copy todir="${3rdParty.destination}">
|
||||
<fileset dir="${3rdParty.home}">
|
||||
<patternset refid="include-3rdparty-patternset-${platform}" />
|
||||
</fileset>
|
||||
</copy>
|
||||
<copy todir="${3rdParty.destination}">
|
||||
<fileset dir="${3rdParty.home}">
|
||||
<patternset id="include-3rdparty-patternset-non-shipped">
|
||||
<include name="FbxSdk/**"/>
|
||||
</patternset>
|
||||
</fileset>
|
||||
</copy>
|
||||
|
||||
|
||||
</target>
|
||||
</project>
|
||||
@@ -1,102 +0,0 @@
|
||||
"""
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
'''
|
||||
Usage:
|
||||
Use EC2 role to download files to %WORKSPACE% folder from bucket bucket_name:
|
||||
python download_from_s3.py --base_dir %WORKSPACE% --files_to_download "file1,file2" --bucket bucket_name
|
||||
|
||||
Use profile to download files to %WORKSPACE% folder from bucket bucket_name:
|
||||
python download_from_s3.py --base_dir %WORKSPACE% --profile profile --files_to_download "file1,file2" --bucket bucket_name
|
||||
'''
|
||||
|
||||
|
||||
import os
|
||||
import json
|
||||
import boto3
|
||||
from optparse import OptionParser
|
||||
from util import error
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = OptionParser()
|
||||
parser.add_option("--base_dir", dest="base_dir", default=os.getcwd(), help="Base directory to download files, If not given, then current directory is used.")
|
||||
parser.add_option("--files_to_download", dest="files_to_download", default=None, help="Files to download, separated by comma.")
|
||||
parser.add_option("--profile", dest="profile", default=None, help="The name of a profile to use. If not given, then the default profile is used.")
|
||||
parser.add_option("--bucket", dest="bucket", default=None, help="S3 bucket the files are downloaded from.")
|
||||
parser.add_option("--key_prefix", dest="key_prefix", default='', help="Object key prefix.")
|
||||
'''
|
||||
ExtraArgs used to call s3.download_file(), should be in json format. extra_args key must be one of: ACL, CacheControl, ContentDisposition, ContentEncoding, ContentLanguage, ContentType, Expires,
|
||||
GrantFullControl, GrantRead, GrantReadACP, GrantWriteACP, Metadata, RequestPayer, ServerSideEncryption, StorageClass,
|
||||
SSECustomerAlgorithm, SSECustomerKey, SSECustomerKeyMD5, SSEKMSKeyId, WebsiteRedirectLocation
|
||||
'''
|
||||
parser.add_option("--extra_args", dest="extra_args", default=None, help="Additional parameters used to download file.")
|
||||
parser.add_option("--max_retry", dest="max_retry", default=1, help="Maximum retry times to download file.")
|
||||
(options, args) = parser.parse_args()
|
||||
if not os.path.isdir(options.base_dir):
|
||||
error('{} is not a valid directory'.format(options.base_dir))
|
||||
if not options.files_to_download:
|
||||
error('Use --files_to_download to specify files to download, separated by comma.')
|
||||
if not options.bucket:
|
||||
error('Use --bucket to specify bucket that the files are downloaded from.')
|
||||
return options
|
||||
|
||||
|
||||
def get_client(service_name, profile_name=None):
|
||||
session = boto3.session.Session(profile_name=profile_name)
|
||||
client = session.client(service_name)
|
||||
return client
|
||||
|
||||
|
||||
def s3_download_file(client, base_dir, file, bucket, key_prefix=None, extra_args=None, max_retry=1):
|
||||
print 'Downloading file {} from bucket {}.'.format(file, bucket)
|
||||
key = file if key_prefix is None else '{}/{}'.format(key_prefix, file)
|
||||
for x in range(max_retry):
|
||||
try:
|
||||
client.download_file(
|
||||
bucket, key, os.path.join(base_dir, file),
|
||||
ExtraArgs=extra_args
|
||||
)
|
||||
print 'Download succeeded'
|
||||
return True
|
||||
except:
|
||||
print 'Retrying download...'
|
||||
print 'Download failed'
|
||||
return False
|
||||
|
||||
|
||||
def download_files(base_dir, files_to_download, bucket, key_prefix=None, profile=None, extra_args=None, max_retry=1):
|
||||
client = get_client('s3', profile)
|
||||
files_to_download = files_to_download.split(',')
|
||||
extra_args = json.loads(extra_args) if extra_args else None
|
||||
|
||||
print 'Downloading {} files from bucket {}.'.format(len(files_to_download), bucket)
|
||||
failure = []
|
||||
success = []
|
||||
for file in files_to_download:
|
||||
if not s3_download_file(client, base_dir, file, bucket, key_prefix, extra_args, max_retry):
|
||||
failure.append(file)
|
||||
else:
|
||||
success.append(file)
|
||||
print '{} files are downloaded successfully:'.format(len(success))
|
||||
print '\n'.join(success)
|
||||
print '{} files failed to download:'.format(len(failure))
|
||||
print '\n'.join(failure)
|
||||
# Exit with error code 1 if any file is failed to download
|
||||
if len(failure) > 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
options = parse_args()
|
||||
download_files(options.base_dir, options.files_to_download, options.bucket, options.key_prefix, options.profile, options.extra_args, options.max_retry)
|
||||
@@ -1,129 +0,0 @@
|
||||
"""
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
"""
|
||||
This script will be used in https://jenkins.agscollab.com/view/%7ESandbox/job/PACKAGE_COPY_S3/
|
||||
PACKAGE_COPY_S3 is a downstream job of nightly packaging job, it copies the nightly packages from Infra S3 bucket to Lionbridge S3 bucket based on the INCLUDE_FILTER passed from packaging job
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
import boto3
|
||||
from util import error, warn
|
||||
|
||||
|
||||
# Write EMAIL_TEMPLATE to a file and inject it into the email sent to Lionbridge
|
||||
EMAIL_TEMPLATE = '''Packages are uploaded to S3 bucket {}
|
||||
Package List:
|
||||
{}
|
||||
|
||||
|
||||
Changelists:
|
||||
{}
|
||||
'''
|
||||
|
||||
|
||||
def get_jenkins_env(key):
|
||||
try:
|
||||
return os.environ[key]
|
||||
except KeyError:
|
||||
print 'Error: Jenkins parameters {} is not set.'.format(key)
|
||||
return None
|
||||
|
||||
|
||||
JENKINS_USERNAME = get_jenkins_env('JENKINS_USERNAME')
|
||||
JENKINS_API_TOKEN = get_jenkins_env('JENKINS_API_TOKEN')
|
||||
JENKINS_URL = get_jenkins_env('JENKINS_URL')
|
||||
WORKSPACE = get_jenkins_env('WORKSPACE')
|
||||
S3_TARGET = get_jenkins_env('S3_TARGET')
|
||||
INCLUDE_FILTER = get_jenkins_env('INCLUDE_FILTER')
|
||||
EMAIL_TEMPLATE_FILE = get_jenkins_env('EMAIL_TEMPLATE_FILE')
|
||||
if None in [JENKINS_USERNAME, JENKINS_API_TOKEN, JENKINS_URL, WORKSPACE, S3_TARGET, INCLUDE_FILTER, EMAIL_TEMPLATE_FILE]:
|
||||
error('Please make sure all Jenkins parameters are set correctly.')
|
||||
|
||||
|
||||
def parse_include_filter(include_filter):
|
||||
try:
|
||||
res = re.search('^(\w*)-*lumberyard-(\d+)\.(\d+)-(\d+)-(\w+).*\*(\d+)\.\*', include_filter)
|
||||
branch = res.group(1)
|
||||
major_version = int(res.group(2))
|
||||
minor_version = int(res.group(3))
|
||||
changelist_number = res.group(4)
|
||||
platform = res.group(5)
|
||||
build_number = res.group(6)
|
||||
return branch, major_version, minor_version, changelist_number, platform, build_number
|
||||
except (AttributeError, IndexError):
|
||||
error('Unable to parse INCLUDE_FILTER, please make sure the INCLUDE_FILTER is set correctly')
|
||||
|
||||
|
||||
# Get the changelists that trigger the build
|
||||
def get_changelists(job_name, build_number):
|
||||
changelists = []
|
||||
headers = {'Content-type': 'application/json', 'Accept': 'application/json'}
|
||||
try:
|
||||
res = requests.get('{}/job/{}/{}/api/json'.format(JENKINS_URL, job_name, build_number),
|
||||
auth=HTTPBasicAuth(JENKINS_USERNAME, JENKINS_API_TOKEN), headers=headers, verify=False)
|
||||
res = json.loads(res.content)
|
||||
changelists = res.get('changeSet').get('items')
|
||||
return changelists
|
||||
except:
|
||||
warn('Error: Failed to get changes from build {} in job {}'.format(build_number, job_name))
|
||||
return []
|
||||
|
||||
|
||||
def get_packaging_job_name(branch, major_version, minor_version, platform):
|
||||
if branch == '':
|
||||
branch = 'ML' if major_version + minor_version == 0 else 'v{}_{}'.format(major_version, minor_version)
|
||||
job_name = 'PKG_{}_{}'.format(branch, platform.capitalize())
|
||||
return job_name
|
||||
|
||||
|
||||
# Get package names by looking up S3 bucket
|
||||
def get_package_names(branch, major_version, minor_version, include_filter, build_number):
|
||||
package_names = []
|
||||
prefix = include_filter[:include_filter.find('*')]
|
||||
pattern = '.*{}.*{}..*'.format(prefix, build_number)
|
||||
if branch == '':
|
||||
bucket_name = 'ly-packages-mainline' if major_version + minor_version == 0 else 'ly-packages-release-candidate'
|
||||
folder = 'lumberyard-packages'
|
||||
else:
|
||||
bucket_name = 'ly-packages-feature-branches'
|
||||
folder = 'lumberyard-packages/{}'.format(branch)
|
||||
s3 = boto3.resource('s3')
|
||||
bucket = s3.Bucket(bucket_name)
|
||||
for obj in bucket.objects.filter(Prefix='{}/{}'.format(folder, prefix)):
|
||||
package_name = obj.key
|
||||
if re.match(pattern, package_name):
|
||||
package_names.append(package_name.replace('{}/'.format(folder), ''))
|
||||
return package_names
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
branch, major_version, minor_version, changelist_number, platform, build_number = parse_include_filter(INCLUDE_FILTER)
|
||||
packaging_job_name = get_packaging_job_name(branch, major_version, minor_version, platform)
|
||||
changelists = get_changelists(packaging_job_name, build_number)
|
||||
package_names = get_package_names(branch, major_version, minor_version, INCLUDE_FILTER, build_number)
|
||||
with open(os.path.join(WORKSPACE, EMAIL_TEMPLATE_FILE), 'w+') as output:
|
||||
if len(package_names) > 0:
|
||||
package_list_str = '\n'.join(package_names)
|
||||
changelists_str = ''
|
||||
for item in changelists:
|
||||
changelists_str += '---------------------------------------------------------------------------------------------\n'
|
||||
try:
|
||||
changelists_str += 'CL{} by {} on {}\n{}\n'.format(item['changeNumber'], item['author']['fullName'], item['changeTime'], item['msg'].encode('utf-8', 'ignore'))
|
||||
except KeyError:
|
||||
error('Internal error, check the output of Jenkins API.')
|
||||
output.write(EMAIL_TEMPLATE.format(S3_TARGET, package_list_str, changelists_str))
|
||||
|
||||
|
||||
@@ -1,662 +0,0 @@
|
||||
"""
|
||||
|
||||
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 ast
|
||||
import boto3
|
||||
import datetime
|
||||
import urllib2
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import traceback
|
||||
import shutil
|
||||
import platform
|
||||
import stat
|
||||
|
||||
IAM_ROLE_NAME = 'ec2-jenkins-node'
|
||||
|
||||
if os.name == 'nt':
|
||||
import ctypes
|
||||
import win32api
|
||||
import collections
|
||||
import locale
|
||||
|
||||
locale.setlocale(locale.LC_ALL, '') # set locale to default to get thousands separators
|
||||
|
||||
PULARGE_INTEGER = ctypes.POINTER(ctypes.c_ulonglong) # Pointer to large unsigned integer
|
||||
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
|
||||
kernel32.GetDiskFreeSpaceExW.argtypes = (ctypes.c_wchar_p,) + (PULARGE_INTEGER,) * 3
|
||||
|
||||
class UsageTuple(collections.namedtuple('UsageTuple', 'total, used, free')):
|
||||
def __str__(self):
|
||||
# Add thousands separator to numbers displayed
|
||||
return self.__class__.__name__ + '(total={:n}, used={:n}, free={:n})'.format(*self)
|
||||
|
||||
def is_dir_symlink(path):
|
||||
FILE_ATTRIBUTE_REPARSE_POINT = 0x0400
|
||||
return os.path.isdir(path) and (ctypes.windll.kernel32.GetFileAttributesW(unicode(path)) & FILE_ATTRIBUTE_REPARSE_POINT)
|
||||
|
||||
def get_free_space_mb(path):
|
||||
if sys.version_info < (3,): # Python 2?
|
||||
saved_conversion_mode = ctypes.set_conversion_mode('mbcs', 'strict')
|
||||
else:
|
||||
try:
|
||||
path = os.fsdecode(path) # allows str or bytes (or os.PathLike in Python 3.6+)
|
||||
except AttributeError: # fsdecode() not added until Python 3.2
|
||||
pass
|
||||
|
||||
# Define variables to receive results when passed as "by reference" arguments
|
||||
_, total, free = ctypes.c_ulonglong(), ctypes.c_ulonglong(), ctypes.c_ulonglong()
|
||||
|
||||
success = kernel32.GetDiskFreeSpaceExW(
|
||||
path, ctypes.byref(_), ctypes.byref(total), ctypes.byref(free))
|
||||
if not success:
|
||||
error_code = ctypes.get_last_error()
|
||||
|
||||
if sys.version_info < (3,): # Python 2?
|
||||
ctypes.set_conversion_mode(*saved_conversion_mode) # restore conversion mode
|
||||
|
||||
if not success:
|
||||
windows_error_message = ctypes.FormatError(error_code)
|
||||
raise ctypes.WinError(error_code, '{} {!r}'.format(windows_error_message, path))
|
||||
|
||||
used = total.value - free.value
|
||||
|
||||
return free.value / 1024 / 1024#for now
|
||||
else:
|
||||
def get_free_space_mb(dirname):
|
||||
st = os.statvfs(dirname)
|
||||
return st.f_bavail * st.f_frsize / 1024 / 1024
|
||||
|
||||
|
||||
def get_iam_role_credentials(role_name):
|
||||
security_metadata = None
|
||||
try:
|
||||
response = urllib2.urlopen(
|
||||
'http://169.254.169.254/latest/meta-data/iam/security-credentials/{0}'.format(role_name)).read()
|
||||
security_metadata = ast.literal_eval(response)
|
||||
except:
|
||||
print 'Unable to get iam role credentials'
|
||||
print traceback.print_exc()
|
||||
|
||||
return security_metadata
|
||||
|
||||
|
||||
def create_volume(ec2_client, availability_zone, project_name, volume_counter):
|
||||
response = ec2_client.create_volume(
|
||||
AvailabilityZone=availability_zone,
|
||||
Size=300,
|
||||
VolumeType='gp2',
|
||||
TagSpecifications=
|
||||
[
|
||||
{
|
||||
'ResourceType': 'volume',
|
||||
'Tags':
|
||||
[
|
||||
{
|
||||
'Key': 'Name',
|
||||
'Value': '{0}'.format(project_name)
|
||||
},
|
||||
{
|
||||
'Key': 'VolumeCounter',
|
||||
'Value': str(volume_counter)
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
)
|
||||
print response
|
||||
volume_id = response['VolumeId']
|
||||
|
||||
# give some time for the creation call to complete
|
||||
time.sleep(1)
|
||||
|
||||
response = ec2_client.describe_volumes(VolumeIds=[volume_id, ])
|
||||
while (response['Volumes'][0]['State'] != 'available'):
|
||||
time.sleep(1)
|
||||
response = ec2_client.describe_volumes(VolumeIds=[volume_id, ])
|
||||
|
||||
return volume_id
|
||||
|
||||
|
||||
def delete_volume(ec2_client, volume_id):
|
||||
response = ec2_client.delete_volume(VolumeId=volume_id)
|
||||
|
||||
|
||||
def unmount_build_volume_from_node():
|
||||
if os.name == 'nt':
|
||||
f = tempfile.NamedTemporaryFile(delete=False)
|
||||
f.write("""
|
||||
select disk 1
|
||||
offline disk
|
||||
""")
|
||||
f.close()
|
||||
|
||||
subprocess.call('diskpart /s %s' % f.name)
|
||||
|
||||
os.unlink(f.name)
|
||||
else:
|
||||
subprocess.call(['umount', '/data'])
|
||||
|
||||
|
||||
def detach_volume_from_node(ec2_client, volume, instance_id, force):
|
||||
ec2_client.delete_tags(Resources=[volume.volume_id],
|
||||
Tags=[
|
||||
{
|
||||
'Key': 'jenkins_attachment_node',
|
||||
},
|
||||
{
|
||||
'Key': 'jenkins_attachment_time',
|
||||
},
|
||||
{
|
||||
'Key': 'jenkins_attachment_build'
|
||||
}
|
||||
])
|
||||
|
||||
incremental_keys = ['jenkins_attachment_node', 'jenkins_attachment_time', 'jenkins_attachment_build']
|
||||
|
||||
volume.load()
|
||||
|
||||
print 'searching for keys adding during incremental build: {}'.format(incremental_keys)
|
||||
|
||||
while len(incremental_keys):
|
||||
tag_keys = set()
|
||||
for tag in volume.tags:
|
||||
tag_keys.add(tag['Key'])
|
||||
|
||||
print 'found tags on instace {}'.format(tag_keys)
|
||||
|
||||
for incremental_key in list(incremental_keys):
|
||||
if incremental_key not in tag_keys:
|
||||
print 'incremental key {} has been successfully removed'.format(incremental_key)
|
||||
incremental_keys.remove(incremental_key)
|
||||
|
||||
volume.load()
|
||||
|
||||
volume.detach_from_instance(Device='xvdf',
|
||||
Force=force,
|
||||
InstanceId=instance_id,
|
||||
VolumeId=volume.volume_id)
|
||||
|
||||
while (len(volume.attachments) and volume.attachments[0]['State'] != 'detached'):
|
||||
time.sleep(1)
|
||||
volume.load()
|
||||
|
||||
volume.load()
|
||||
|
||||
if (len(volume.attachments)):
|
||||
print 'Volume still has attachments'
|
||||
for attachment in volume.attachments:
|
||||
print 'Volume {} {} to instance {}'.format(attachment['VolumeId'], attachment['State'], attachment['InstanceId'])
|
||||
|
||||
|
||||
def cleanup_node(workspace_name):
|
||||
if os.name == 'nt':
|
||||
jenkins_base = os.getenv('BASE')
|
||||
dev_path = '{}\\workspace\\{}\\dev'.format(jenkins_base, workspace_name)
|
||||
else:
|
||||
dev_path = '/home/lybuilder/ly/workspace/{}/dev'.format(workspace_name)
|
||||
|
||||
if os.path.exists(dev_path):
|
||||
if os.name == 'nt':
|
||||
if is_dir_symlink(dev_path):
|
||||
print "removing symlink path {}".format(dev_path)
|
||||
os.rmdir(dev_path)
|
||||
else:
|
||||
# this shouldn't happen, but is here for sanity's sake, if we sync to the build node erroneously we want to clean it up if we can
|
||||
print "given symlink path was not a symlink, deleting the full tree to prevent future build failures"
|
||||
retcode = os.system('rmdir /S /Q {}'.format(dev_path))
|
||||
if retcode != 0:
|
||||
raise Exception("rmdir failed to remove directory: {}".format(dev_path))
|
||||
return True
|
||||
else:
|
||||
if os.path.islink(dev_path):
|
||||
print "unlinking symlink path {}".format(dev_path)
|
||||
os.unlink(dev_path)
|
||||
else:
|
||||
print "given symlink path was not a symlink, deleting the full tree to prevent future build failures"
|
||||
os.chmod(dev_path, stat.S_IWUSR)
|
||||
shutil.rmtree(dev_path, ignore_errors=True)
|
||||
return True
|
||||
# check to make sure the directory was actually deleted
|
||||
if os.path.exists(dev_path):
|
||||
raise Exception("Failed to remove directory: {}".format(dev_path))
|
||||
return False
|
||||
|
||||
|
||||
def setup_volume(workspace_name, created):
|
||||
if os.name == 'nt':
|
||||
f = tempfile.NamedTemporaryFile(delete=False)
|
||||
f.write("""
|
||||
select disk 1
|
||||
online disk
|
||||
attribute disk clear readonly
|
||||
""") # assume disk # for now
|
||||
|
||||
if created:
|
||||
f.write("""create partition primary
|
||||
select partition 1
|
||||
format quick fs=ntfs
|
||||
assign
|
||||
active
|
||||
""")
|
||||
|
||||
f.close()
|
||||
|
||||
subprocess.call(['diskpart', '/s', f.name])
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
drives_after = win32api.GetLogicalDriveStrings()
|
||||
drives_after = drives_after.split('\000')[:-1]
|
||||
|
||||
print drives_after
|
||||
|
||||
#drive_letter = next(item for item in drives_after if item not in drives_before)
|
||||
drive_letter = 'D:\\'
|
||||
|
||||
os.unlink(f.name)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
dev_path = '{}ly\workspace\{}\dev'.format(drive_letter, workspace_name)
|
||||
|
||||
else:
|
||||
subprocess.call(['file', '-s', '/dev/xvdf'])
|
||||
if created:
|
||||
subprocess.call(['mkfs', '-t', 'ext4', '/dev/xvdf'])
|
||||
subprocess.call(['mount', '/dev/xvdf', '/data'])
|
||||
|
||||
dev_path = '/data/ly/workspace/{}/dev'.format(workspace_name)
|
||||
|
||||
return dev_path
|
||||
|
||||
|
||||
def attach_volume_to_instance(volume, volume_id, instance_id, instance_name):
|
||||
volume.attach_to_instance(Device='xvdf',
|
||||
InstanceId=instance_id,
|
||||
VolumeId=volume_id)
|
||||
# give a little bit of time for the aws call to process
|
||||
time.sleep(2)
|
||||
|
||||
# reload the volume just in case
|
||||
volume.load()
|
||||
|
||||
while (len(volume.attachments) and volume.attachments[0]['State'] != 'attached'):
|
||||
time.sleep(1)
|
||||
volume.load()
|
||||
|
||||
volume.create_tags(Tags=[
|
||||
{
|
||||
'Key':'last_attachment_time',
|
||||
'Value':datetime.datetime.utcnow().isoformat()
|
||||
}
|
||||
])
|
||||
|
||||
volume.create_tags(Tags=[
|
||||
{
|
||||
'Key':'jenkins_attachment_node',
|
||||
'Value':instance_name,
|
||||
},
|
||||
{
|
||||
'Key':'jenkins_attachment_time',
|
||||
'Value':datetime.datetime.utcnow().isoformat()
|
||||
},
|
||||
{
|
||||
'Key':'jenkins_attachment_build',
|
||||
'Value':os.getenv('BUILD_TAG')
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
def prepare_incremental_build(workspace_name):
|
||||
job_name = os.getenv('JOB_NAME', None)
|
||||
clean_build = os.getenv('CLEAN_BUILD', 'false').lower() == 'true'
|
||||
|
||||
android_home = os.getenv('ANDROID_HOME', None)
|
||||
if android_home is not None:
|
||||
path = os.getenv('PATH').split(';')
|
||||
print path
|
||||
|
||||
java_home = os.getenv('JAVA_HOME', None)
|
||||
print java_home
|
||||
|
||||
os.environ['LY_NDK_PATH'] = 'C:\\ly\\3rdParty\\android-ndk\\r12'
|
||||
print os.getenv('LY_NDK_PATH')
|
||||
|
||||
path = [x for x in path if not (java_home in x or android_home in x)]
|
||||
print path
|
||||
|
||||
path.append(java_home)
|
||||
path.append(android_home)
|
||||
path.append(os.getenv('LY_NDK_PATH'))
|
||||
print path
|
||||
|
||||
os.environ['PATH'] = ';'.join(path)
|
||||
|
||||
credentials = get_iam_role_credentials(IAM_ROLE_NAME)
|
||||
|
||||
aws_access_key_id = None
|
||||
aws_secret_access_key = None
|
||||
aws_session_token = None
|
||||
|
||||
if credentials is not None:
|
||||
keys = ['AccessKeyId', 'SecretAccessKey', 'Token']
|
||||
for key in keys:
|
||||
if key not in credentials:
|
||||
print 'Unable to find {0} in get_iam_role_credentials response {1}'.format(key, credentials)
|
||||
return
|
||||
|
||||
aws_access_key_id = credentials['AccessKeyId']
|
||||
aws_secret_access_key = credentials['SecretAccessKey']
|
||||
aws_session_token = credentials['Token']
|
||||
|
||||
session = boto3.session.Session()
|
||||
region = session.region_name
|
||||
|
||||
try:
|
||||
instance_id = urllib2.urlopen('http://169.254.169.254/latest/meta-data/instance-id').read()
|
||||
except:
|
||||
# this likely means we're not an ec2 instance
|
||||
raise Exception('No EC2 metadata!')
|
||||
|
||||
|
||||
try:
|
||||
availability_zone = urllib2.urlopen(
|
||||
'http://169.254.169.254/latest/meta-data/placement/availability-zone').read()
|
||||
except:
|
||||
# also likely means we're not an ec2 instance
|
||||
raise Exception('No EC2 metadata')
|
||||
|
||||
|
||||
if region is None:
|
||||
region = 'us-west-2'
|
||||
|
||||
client = boto3.client('ec2', region_name=region, aws_access_key_id=aws_access_key_id,
|
||||
aws_secret_access_key=aws_secret_access_key,
|
||||
aws_session_token=aws_session_token)
|
||||
|
||||
project_name = job_name
|
||||
|
||||
ec2_resource = boto3.resource('ec2', region_name=region)
|
||||
instance = ec2_resource.Instance(instance_id)
|
||||
|
||||
volume_counter = 0
|
||||
|
||||
for volume in instance.volumes.all():
|
||||
for attachment in volume.attachments:
|
||||
print 'attachment device: {}'.format(attachment['Device'])
|
||||
if 'xvdf' in attachment['Device'] and attachment['State'] != 'detached':
|
||||
print 'A device is already attached to xvdf. This likely means a previous build failed to detach it\'s' \
|
||||
'build volume. This volume is considered orphaned and will be force detached from this instance.'
|
||||
unmount_build_volume_from_node()
|
||||
detach_volume_from_node(client, volume, instance_id, True)
|
||||
|
||||
if cleanup_node(workspace_name):
|
||||
clean_build = True
|
||||
|
||||
response = client.describe_volumes(Filters=
|
||||
[
|
||||
{
|
||||
'Name': 'tag:Name',
|
||||
'Values':
|
||||
[
|
||||
'{0}'.format(project_name)
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
created = False
|
||||
|
||||
if 'Volumes' in response and not len(response['Volumes']):
|
||||
print 'Volume for {0} doesn\'t exist creating it...'.format(project_name)
|
||||
# volume doesn't exist, create it
|
||||
volume_id = create_volume(client, availability_zone, project_name, volume_counter)
|
||||
created = True
|
||||
|
||||
elif len(response['Volumes']) > 1:
|
||||
latest_volume = None
|
||||
max_counter = 0
|
||||
|
||||
for volume in response['Volumes']:
|
||||
for tag in volume['Tags']:
|
||||
if tag['Key'] == 'VolumeCounter':
|
||||
if int(tag['Value']) > max_counter:
|
||||
max_counter = int(tag['Value'])
|
||||
latest_volume = volume
|
||||
|
||||
volume_counter = max_counter
|
||||
volume_id = latest_volume['VolumeId']
|
||||
else:
|
||||
volume = response['Volumes'][0]
|
||||
if len(volume['Attachments']):
|
||||
# this is bad we shouldn't be attached, we should have detached at the end of a build
|
||||
attachment = volume['Attachments'][0]
|
||||
print ('Volume already has attachment {}'.format(attachment))
|
||||
print 'Creating new volume for {} and orphaning previous volume'.format(project_name)
|
||||
|
||||
for tag in volume['Tags']:
|
||||
if tag['Key'] == 'VolumeCounter':
|
||||
volume_counter = int(tag['Value']) + 1
|
||||
break
|
||||
|
||||
volume_id = create_volume(client, availability_zone, project_name, volume_counter)
|
||||
created = True
|
||||
else:
|
||||
volume_id = volume['VolumeId']
|
||||
|
||||
if clean_build and not created:
|
||||
print 'CLEAN_BUILD option was set, deleting volume {0}'.format(volume_id)
|
||||
revert_workspace(job_name)
|
||||
delete_volume(client, volume_id)
|
||||
volume_id = create_volume(client, availability_zone, project_name, volume_counter)
|
||||
created = True
|
||||
|
||||
print 'attaching volume {} to instance {}'.format(volume_id, instance_id)
|
||||
volume = ec2_resource.Volume(volume_id)
|
||||
|
||||
instance_name = next(tag['Value'] for tag in instance.tags if tag['Key'] == 'Name')
|
||||
|
||||
if os.name == 'nt':
|
||||
drives_before = win32api.GetLogicalDriveStrings()
|
||||
drives_before = drives_before.split('\000')[:-1]
|
||||
|
||||
print drives_before
|
||||
|
||||
attach_volume_to_instance(volume, volume_id, instance_id, instance_name)
|
||||
|
||||
dev_path = setup_volume(workspace_name, created)
|
||||
|
||||
dev_existed = True
|
||||
|
||||
if os.name == 'nt':
|
||||
free_space_path = 'D:\\'
|
||||
else:
|
||||
free_space_path = '/data/'
|
||||
|
||||
if get_free_space_mb(free_space_path) < 1024:
|
||||
print 'Volume is running low on disk space. Recreating volume and running clean build.'
|
||||
unmount_build_volume_from_node()
|
||||
detach_volume_from_node(client, volume, instance_id, False)
|
||||
delete_volume(client, volume_id)
|
||||
|
||||
volume_id = create_volume(client, availability_zone, project_name, volume_counter)
|
||||
volume = ec2_resource.Volume(volume_id)
|
||||
attach_volume_to_instance(volume, volume_id, instance_id, instance_name)
|
||||
setup_volume(workspace_name, True)
|
||||
|
||||
if not os.path.exists(dev_path):
|
||||
print 'creating directory structure for {}'.format(dev_path)
|
||||
os.makedirs(dev_path)
|
||||
if os.name != 'nt':
|
||||
print 'taking ownership of {}'.format(dev_path)
|
||||
subprocess.call(['chown', '-R', 'lybuilder:root', dev_path])
|
||||
dev_existed = False
|
||||
|
||||
if os.name == 'nt':
|
||||
jenkins_base = os.getenv('BASE')
|
||||
try:
|
||||
symlink_path = '{}\\workspace\\{}\\dev'.format(jenkins_base, workspace_name)
|
||||
print 'creating symlink to path: {}'.format(symlink_path)
|
||||
subprocess.call(['cmd', '/c', 'mklink', '/J', symlink_path, dev_path])
|
||||
#subprocess.call(['cmd', '/c', 'mklink', '/J', '{}\\3rdParty'.format(jenkins_base), 'E:\\3rdParty'])
|
||||
except Exception as e:
|
||||
print e
|
||||
else:
|
||||
subprocess.call(['ln', '-s', '-f', dev_path, '/home/lybuilder/ly/workspace/{}'.format(workspace_name)])
|
||||
subprocess.call(['ln', '-s', '-f', '/home/lybuilder/ly/workspace/3rdParty', '/data/ly/workspace'])
|
||||
|
||||
if not dev_existed:
|
||||
print 'flushing perforce #have revision'
|
||||
subprocess.call(['p4', 'trust'])
|
||||
subprocess.call(['p4', 'flush', '-f', '//ly_jenkins_{}/dev/...#none'.format(job_name)])
|
||||
#subprocess.call(['p4', 'sync', '-f', '//ly_jenkins_{}/dev/...'.format(job_name)])
|
||||
|
||||
|
||||
def revert_workspace(job_name):
|
||||
try:
|
||||
# Workaround for LY-86789: Revert bootstrap.cfg checkout.
|
||||
print "REVERTING workspace {}".format(job_name)
|
||||
subprocess.check_call(['p4', 'revert', '//ly_jenkins_{}/dev/...'.format(job_name)])
|
||||
except subprocess.CalledProcessError as e:
|
||||
print e.output
|
||||
raise e
|
||||
except Exception as e:
|
||||
print e
|
||||
raise e
|
||||
|
||||
|
||||
def teardown_incremental_build(workspace_name):
|
||||
job_name = os.getenv('JOB_NAME', None)
|
||||
|
||||
if os.path.isfile('envinject.properties'):
|
||||
os.remove('envinject.properties')
|
||||
|
||||
credentials = get_iam_role_credentials(IAM_ROLE_NAME)
|
||||
|
||||
aws_access_key_id = None
|
||||
aws_secret_access_key = None
|
||||
aws_session_token = None
|
||||
|
||||
if credentials is not None:
|
||||
keys = ['AccessKeyId', 'SecretAccessKey', 'Token']
|
||||
for key in keys:
|
||||
if key not in credentials:
|
||||
raise Exception('Unable to find {0} in get_iam_role_credentials response {1}'.format(key, credentials))
|
||||
|
||||
aws_access_key_id = credentials['AccessKeyId']
|
||||
aws_secret_access_key = credentials['SecretAccessKey']
|
||||
aws_session_token = credentials['Token']
|
||||
|
||||
session = boto3.session.Session()
|
||||
region = session.region_name
|
||||
|
||||
try:
|
||||
instance_id = urllib2.urlopen('http://169.254.169.254/latest/meta-data/instance-id').read()
|
||||
except:
|
||||
# this likely means we're not an ec2 instance
|
||||
raise Exception('No EC2 metadata!')
|
||||
|
||||
if region is None:
|
||||
region = 'us-west-2'
|
||||
|
||||
client = boto3.client('ec2', region_name=region, aws_access_key_id=aws_access_key_id,
|
||||
aws_secret_access_key=aws_secret_access_key,
|
||||
aws_session_token=aws_session_token)
|
||||
|
||||
project_name = job_name
|
||||
response = client.describe_volumes(Filters=
|
||||
[
|
||||
{
|
||||
'Name': 'tag:Name',
|
||||
'Values':
|
||||
[
|
||||
'{0}'.format(project_name)
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
ec2_resource = boto3.resource('ec2', region_name=region)
|
||||
instance = ec2_resource.Instance(instance_id)
|
||||
|
||||
volume = None
|
||||
|
||||
for attached_volume in instance.volumes.all():
|
||||
for attachment in attached_volume.attachments:
|
||||
print 'attachment device: {}'.format(attachment['Device'])
|
||||
if attachment['Device'] == 'xvdf':
|
||||
volume = attached_volume
|
||||
|
||||
if volume is None:
|
||||
# volume doesn't exist, do nothing
|
||||
print 'Volume for {} does not exist or is not attached to the current instance. This probably isn\'t an issue but should be reported.'.format(project_name)
|
||||
return
|
||||
else:
|
||||
revert_workspace(job_name)
|
||||
|
||||
unmount_build_volume_from_node()
|
||||
|
||||
detach_volume_from_node(client, volume, instance_id, False)
|
||||
|
||||
cleanup_node(workspace_name)
|
||||
|
||||
|
||||
def prepare_incremental_build_mac(workspace_name):
|
||||
job_name = os.getenv('JOB_NAME', None)
|
||||
clean_build = os.getenv('CLEAN_BUILD', 'false').lower() == 'true'
|
||||
|
||||
subprocess.call(['mount', '-t', 'smbfs', '//lybuilder:Builder99@gt-sna11-nas-01.local/inc-build/ly', '/data/ly'])
|
||||
|
||||
dev_path = '/data/ly/workspace/{}/dev'.format(workspace_name)
|
||||
|
||||
dev_existed = True
|
||||
|
||||
if clean_build:
|
||||
print 'cleaning {}'.format(dev_path)
|
||||
subprocess.call(['rm', '-rf', dev_path])
|
||||
if not os.path.exists(dev_path):
|
||||
print 'creating directory structure for {}'.format(dev_path)
|
||||
os.makedirs(dev_path)
|
||||
dev_existed = False
|
||||
|
||||
#subprocess.call(['ln', '-s', '-f', '/data/ly/workspace', '/Users/lybuilder'])
|
||||
|
||||
#subprocess.call(['ln', '-s', '-f', '/Users/lybuilder/workspace/3rdParty', '/data/ly/workspace'])
|
||||
|
||||
if not dev_existed:
|
||||
print 'flushing perforce #have revision'
|
||||
subprocess.call(['p4', 'trust'])
|
||||
subprocess.call(['p4', 'flush', '-f', '//ly_jenkins_{}/dev/...#none'.format(job_name)])
|
||||
|
||||
|
||||
def main():
|
||||
action = sys.argv[1]
|
||||
workspace_name = sys.argv[2]
|
||||
|
||||
if action.lower() == 'prepare':
|
||||
if platform.system().lower() == 'darwin':
|
||||
prepare_incremental_build_mac(workspace_name)
|
||||
else:
|
||||
prepare_incremental_build(workspace_name)
|
||||
elif action.lower() == 'teardown':
|
||||
if platform.system().lower() == 'darwin':
|
||||
pass
|
||||
else:
|
||||
teardown_incremental_build(workspace_name)
|
||||
else:
|
||||
'Invalid command. Valid actions are either "prepare" or "teardown."'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,57 +0,0 @@
|
||||
"""
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
'''
|
||||
All this script is doing is writing grabbing a file that was written previously marking the start of when the Perforce would run
|
||||
and then getting the current time to find out how long we spent in Perforce
|
||||
'''
|
||||
|
||||
import time
|
||||
|
||||
from util import *
|
||||
|
||||
|
||||
def write_metrics():
|
||||
enable_build_metrics = os.environ.get('ENABLE_BUILD_METRICS')
|
||||
metrics_namespace = os.environ.get('METRICS_NAMESPACE')
|
||||
if enable_build_metrics == 'true':
|
||||
scm_end = int(time.time())
|
||||
workspace = os.environ.get('WORKSPACE')
|
||||
metrics_file_name = 'scm_start.txt'
|
||||
if workspace is None:
|
||||
safe_exit_with_error('{} must be run in Jenkins job.'.format(os.path.basename(__file__)))
|
||||
try:
|
||||
with open(os.path.join(workspace, metrics_file_name), 'r') as f:
|
||||
scm_start = int(f.readline())
|
||||
except:
|
||||
safe_exit_with_error('Failed to read from {}'.format(metrics_file_name))
|
||||
|
||||
scm_total = scm_end - scm_start
|
||||
|
||||
script_path = os.path.join(workspace, 'dev/Tools/build/waf-1.7.13/build_metrics/write_build_metric.py')
|
||||
|
||||
build_tag = os.environ.get('BUILD_TAG')
|
||||
p4_changelist = os.environ.get('P4_CHANGELIST')
|
||||
|
||||
if build_tag is not None and p4_changelist is not None:
|
||||
os.environ['BUILD_ID'] = '{0}.{1}'.format(build_tag, p4_changelist)
|
||||
|
||||
cwd = os.getcwd()
|
||||
os.chdir(os.path.join(workspace, 'dev'))
|
||||
cmd = 'python {} SCMTime {} Seconds --enable-build-metrics {} --metrics-namespace {} --project-spec None'.format(script_path, scm_total, True, metrics_namespace)
|
||||
# metrics call shouldn't fail the job
|
||||
safe_execute_system_call(cmd, shell=True)
|
||||
os.chdir(cwd)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
write_metrics()
|
||||
@@ -1,12 +0,0 @@
|
||||
"""
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
"""
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
"""
|
||||
Filename globbing utility.
|
||||
Modified using https://github.com/python/cpython/blob/3.7/Lib/glob.py to be compatible with Python2
|
||||
Original file Copyright Python Software Foundation, used under license.
|
||||
Modifications copyright Amazon.com, Inc. or its affiliates.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import fnmatch
|
||||
|
||||
__all__ = ["glob", "iglob", "escape"]
|
||||
|
||||
def glob(pathname, recursive=False):
|
||||
"""Return a list of paths matching a pathname pattern.
|
||||
The pattern may contain simple shell-style wildcards a la
|
||||
fnmatch. However, unlike fnmatch, filenames starting with a
|
||||
dot are special cases that are not matched by '*' and '?'
|
||||
patterns.
|
||||
If recursive is true, the pattern '**' will match any files and
|
||||
zero or more directories and subdirectories.
|
||||
"""
|
||||
return list(iglob(pathname, recursive=recursive))
|
||||
|
||||
def iglob(pathname, recursive=False):
|
||||
"""Return an iterator which yields the paths matching a pathname pattern.
|
||||
The pattern may contain simple shell-style wildcards a la
|
||||
fnmatch. However, unlike fnmatch, filenames starting with a
|
||||
dot are special cases that are not matched by '*' and '?'
|
||||
patterns.
|
||||
If recursive is true, the pattern '**' will match any files and
|
||||
zero or more directories and subdirectories.
|
||||
"""
|
||||
it = _iglob(pathname, recursive, False)
|
||||
if recursive and _isrecursive(pathname):
|
||||
s = next(it) # skip empty string
|
||||
assert not s
|
||||
return it
|
||||
|
||||
def _iglob(pathname, recursive, dironly):
|
||||
dirname, basename = os.path.split(pathname)
|
||||
if not has_magic(pathname):
|
||||
assert not dironly
|
||||
if basename:
|
||||
if os.path.lexists(pathname):
|
||||
yield pathname
|
||||
else:
|
||||
# Patterns ending with a slash should match only directories
|
||||
if os.path.isdir(dirname):
|
||||
yield pathname
|
||||
return
|
||||
if not dirname:
|
||||
if recursive and _isrecursive(basename):
|
||||
yield _glob2(dirname, basename, dironly)
|
||||
else:
|
||||
yield _glob1(dirname, basename, dironly)
|
||||
return
|
||||
# `os.path.split()` returns the argument itself as a dirname if it is a
|
||||
# drive or UNC path. Prevent an infinite recursion if a drive or UNC path
|
||||
# contains magic characters (i.e. r'\\?\C:').
|
||||
if dirname != pathname and has_magic(dirname):
|
||||
dirs = _iglob(dirname, recursive, True)
|
||||
else:
|
||||
dirs = [dirname]
|
||||
if has_magic(basename):
|
||||
if recursive and _isrecursive(basename):
|
||||
glob_in_dir = _glob2
|
||||
else:
|
||||
glob_in_dir = _glob1
|
||||
else:
|
||||
glob_in_dir = _glob0
|
||||
for dirname in dirs:
|
||||
for name in glob_in_dir(dirname, basename, dironly):
|
||||
yield os.path.join(dirname, name)
|
||||
|
||||
# These 2 helper functions non-recursively glob inside a literal directory.
|
||||
# They return a list of basenames. _glob1 accepts a pattern while _glob0
|
||||
# takes a literal basename (so it only has to check for its existence).
|
||||
|
||||
def _glob1(dirname, pattern, dironly):
|
||||
names = list(_iterdir(dirname, dironly))
|
||||
return fnmatch.filter(names, pattern)
|
||||
|
||||
def _glob0(dirname, basename, dironly):
|
||||
if not basename:
|
||||
# `os.path.split()` returns an empty basename for paths ending with a
|
||||
# directory separator. 'q*x/' should match only directories.
|
||||
if os.path.isdir(dirname):
|
||||
return [basename]
|
||||
else:
|
||||
if os.path.lexists(os.path.join(dirname, basename)):
|
||||
return [basename]
|
||||
return []
|
||||
|
||||
# Following functions are not public but can be used by third-party code.
|
||||
|
||||
def glob0(dirname, pattern):
|
||||
return _glob0(dirname, pattern, False)
|
||||
|
||||
def glob1(dirname, pattern):
|
||||
return _glob1(dirname, pattern, False)
|
||||
|
||||
# This helper function recursively yields relative pathnames inside a literal
|
||||
# directory.
|
||||
|
||||
def _glob2(dirname, pattern, dironly):
|
||||
assert _isrecursive(pattern)
|
||||
return [pattern[:0]] + list(_rlistdir(dirname, dironly))
|
||||
|
||||
# If dironly is false, yields all file names inside a directory.
|
||||
# If dironly is true, yields only directory names.
|
||||
def _iterdir(dirname, dironly):
|
||||
if not dirname:
|
||||
if isinstance(dirname, bytes):
|
||||
dirname = bytes(os.curdir, 'ASCII')
|
||||
else:
|
||||
dirname = os.curdir
|
||||
try:
|
||||
for entry in os.listdir(dirname):
|
||||
yield entry
|
||||
except OSError:
|
||||
return
|
||||
|
||||
# Recursively yields relative pathnames inside a literal directory.
|
||||
def _rlistdir(dirname, dironly):
|
||||
if not os.path.islink(dirname):
|
||||
names = list(_iterdir(dirname, dironly))
|
||||
for x in names:
|
||||
yield x
|
||||
path = os.path.join(dirname, x) if dirname else x
|
||||
for y in _rlistdir(path, dironly):
|
||||
yield os.path.join(x, y)
|
||||
magic_check = re.compile('([*?[])')
|
||||
magic_check_bytes = re.compile(b'([*?[])')
|
||||
|
||||
def has_magic(s):
|
||||
if isinstance(s, bytes):
|
||||
match = magic_check_bytes.search(s)
|
||||
else:
|
||||
match = magic_check.search(s)
|
||||
return match is not None
|
||||
|
||||
def _ishidden(path):
|
||||
return path[0] in ('.', b'.'[0])
|
||||
|
||||
def _isrecursive(pattern):
|
||||
if isinstance(pattern, bytes):
|
||||
return pattern == b'**'
|
||||
else:
|
||||
return pattern == '**'
|
||||
|
||||
def escape(pathname):
|
||||
"""Escape all special characters.
|
||||
"""
|
||||
# Escaping is done by wrapping any of "*?[" between square brackets.
|
||||
# Metacharacters do not work in the drive part and shouldn't be escaped.
|
||||
drive, pathname = os.path.splitdrive(pathname)
|
||||
if isinstance(pathname, bytes):
|
||||
pathname = magic_check_bytes.sub(br'[\1]', pathname)
|
||||
else:
|
||||
pathname = magic_check.sub(r'[\1]', pathname)
|
||||
return drive + pathname
|
||||
@@ -1,75 +0,0 @@
|
||||
"""
|
||||
|
||||
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 os
|
||||
import sys
|
||||
from validate_ly_version import _read_version_from_branch_spec
|
||||
from argparse import ArgumentParser
|
||||
|
||||
|
||||
def main(args):
|
||||
waf_branch_spec_file_directory = os.path.join(os.environ['WORKSPACE'], 'dev')
|
||||
waf_branch_spec_file_name = 'waf_branch_spec.py'
|
||||
|
||||
if not os.path.exists(os.path.join(waf_branch_spec_file_directory, waf_branch_spec_file_name)):
|
||||
raise Exception("Invalid workspace directory: {}".format(waf_branch_spec_file_directory))
|
||||
|
||||
waf_branch_spec_version = _read_version_from_branch_spec(waf_branch_spec_file_directory, waf_branch_spec_file_name)
|
||||
if waf_branch_spec_version is None:
|
||||
raise Exception("Unable to read branch spec version from {}.".format(waf_branch_spec_file_name))
|
||||
|
||||
if args.version:
|
||||
waf_branch_spec_version = args.version
|
||||
|
||||
if waf_branch_spec_version=='0.0.0.0' and not args.allow_unversioned:
|
||||
raise Exception('Version "{}" is invalid. Please specify a valid, non-zero LUMBERYARD_VERSION in {}.'.format(
|
||||
waf_branch_spec_version,
|
||||
os.path.join(waf_branch_spec_file_directory, waf_branch_spec_file_name)
|
||||
))
|
||||
|
||||
versions = waf_branch_spec_version.split('.')
|
||||
if len(versions) != 4:
|
||||
raise Exception("Invalid branch spec version '{}'. Must use format 'X.X.X.X'".format(waf_branch_spec_version))
|
||||
|
||||
major_version = versions[0]
|
||||
minor_version = versions[1]
|
||||
|
||||
env_inject_file_path = os.path.join(os.environ['WORKSPACE'], os.environ['ENV_INJECT_FILE'])
|
||||
|
||||
print major_version
|
||||
print minor_version
|
||||
|
||||
with open(env_inject_file_path, 'w') as env_inject_file:
|
||||
env_inject_file.write('MAJOR_VERSION={}\n'.format(major_version))
|
||||
env_inject_file.write('MINOR_VERSION={}\n'.format(minor_version))
|
||||
|
||||
|
||||
def check_env(*vars):
|
||||
missing = []
|
||||
for var in vars:
|
||||
if var not in os.environ:
|
||||
missing += (var,)
|
||||
if missing:
|
||||
raise Exception("Missing one or more environment variables: {}".format(", ".join(missing)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument('--allow-unversioned', default=False, action='store_true',
|
||||
help="Allow version '0.0.0.0'. Default is to fail if an invalid version is found")
|
||||
parser.add_argument('--version', type=str,
|
||||
help="Manually specify version to use, instead of scanning dev root.")
|
||||
args = parser.parse_args()
|
||||
|
||||
check_env("WORKSPACE", "ENV_INJECT_FILE")
|
||||
|
||||
main(args)
|
||||
@@ -1,212 +0,0 @@
|
||||
#
|
||||
# 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 requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from P4 import P4, P4Exception
|
||||
from util import *
|
||||
from zipfile import ZipFile
|
||||
from download_from_s3 import s3_download_file, get_client
|
||||
from botocore.exceptions import ClientError
|
||||
from upload_to_s3 import s3_upload_file
|
||||
import os
|
||||
import shutil
|
||||
import json
|
||||
import urllib
|
||||
|
||||
PACKAGE_NAME_REGEX = r'^lumberyard-\d\.\d-\d+-\w+-\d+\.(zip|tgz)$'
|
||||
P4_USER = 'lybuilder'
|
||||
BUCKET = 'ly-scrubbing-test'
|
||||
|
||||
# Scrubber will fail if any of these files is missing, copy these files to scrubbing workspace before the test
|
||||
SCRUBBING_REQUIRED_FILES = [
|
||||
]
|
||||
|
||||
try:
|
||||
JENKINS_USERNAME = os.environ['JENKINS_USERNAME']
|
||||
JENKINS_API_TOKEN = os.environ['JENKINS_API_TOKEN']
|
||||
JENKINS_SERVER = os.environ['JENKINS_URL']
|
||||
P4_PORT = os.environ['ENV_P4_PORT']
|
||||
JOB_NAME = os.environ['JOB_NAME']
|
||||
BUILD_NUMBER = int(os.environ['BUILD_NUMBER'])
|
||||
WORKSPACE = os.environ['WORKSPACE']
|
||||
SCRUBBING_WORKSPACE = os.environ['SCRUBBING_WORKSPACE']
|
||||
except KeyError:
|
||||
error('This script has to run on Jenkins')
|
||||
|
||||
|
||||
class File:
|
||||
def __init__(self, path, action):
|
||||
self.path = path
|
||||
self.action = action
|
||||
|
||||
|
||||
# Get the changelist numbers that trigger the build
|
||||
def get_changelist_numbers():
|
||||
changelist_numbers = []
|
||||
changeset = []
|
||||
headers = {'Content-type': 'application/json', 'Accept': 'application/json'}
|
||||
try:
|
||||
res = requests.get('{}/job/{}/{}/api/json'.format(JENKINS_SERVER, JOB_NAME, BUILD_NUMBER),
|
||||
auth=HTTPBasicAuth(JENKINS_USERNAME, JENKINS_API_TOKEN), headers=headers, verify=False)
|
||||
res = json.loads(res.content)
|
||||
changeset = res.get('changeSet').get('items')
|
||||
except:
|
||||
print 'Error: Failed to get changes from build {} in job {}'.format(BUILD_NUMBER, JOB_NAME)
|
||||
for item in changeset:
|
||||
changelist_numbers.append(item.get('changeNumber'))
|
||||
return changelist_numbers
|
||||
|
||||
|
||||
# Get file list and actions that trigger the Jenkins job
|
||||
def get_files():
|
||||
p4 = P4()
|
||||
p4.port = P4_PORT
|
||||
p4.user = P4_USER
|
||||
p4.connect()
|
||||
|
||||
files = []
|
||||
changelist_numbers = get_changelist_numbers()
|
||||
for changelist_number in changelist_numbers:
|
||||
cmd = ['describe', '-s', changelist_number]
|
||||
try:
|
||||
res = p4.run(cmd)[0]
|
||||
file_list = res.get('depotFile')
|
||||
actions = res.get('action')
|
||||
for action, file_path in zip(actions, file_list):
|
||||
# P4 returns file paths that are url encoded
|
||||
file_path = urllib.unquote(file_path).decode("utf8")
|
||||
# Ignore files which are not in dev
|
||||
p = file_path.find('dev')
|
||||
if p != -1:
|
||||
files.append(File(file_path[p:], action))
|
||||
except P4Exception:
|
||||
error('Internal error, please contact Build System')
|
||||
return files
|
||||
|
||||
|
||||
def copy_file(src, dst, overwrite=False):
|
||||
if os.path.exists(dst) and not overwrite:
|
||||
return
|
||||
print 'Copying file from {} to {}'.format(src, dst)
|
||||
dest_file_dir = os.path.dirname(dst)
|
||||
if not os.path.exists(dest_file_dir):
|
||||
os.makedirs(dest_file_dir)
|
||||
shutil.copyfile(src, dst)
|
||||
|
||||
|
||||
# Run scrubbing scripts
|
||||
def scrub():
|
||||
print 'Perform the Code Scrubbing'
|
||||
scrubber_path = os.path.join(WORKSPACE, 'dev/Tools/build/JenkinsScripts/distribution/scrubbing/scrub_all.py')
|
||||
scrub_params = ["-p", "-d", "-o"]
|
||||
# Scrub code
|
||||
args = ['python', scrubber_path, '-p', '-d', '-o', os.path.join(SCRUBBING_WORKSPACE, 'dev/Code'), os.path.join(SCRUBBING_WORKSPACE, 'dev')]
|
||||
return_code = safe_execute_system_call(args)
|
||||
if return_code != 0:
|
||||
print 'ERROR: Code scrubbing failed.'
|
||||
return False
|
||||
print 'Code scrubbing complete successfully.'
|
||||
return True
|
||||
|
||||
|
||||
# Run scrubbing validator
|
||||
def validate():
|
||||
# Run validator
|
||||
print 'Running validator'
|
||||
validator_platforms = ["provo", "salem", "jasper"]
|
||||
success = True
|
||||
for validator_platform in validator_platforms:
|
||||
validator_path = os.path.join(WORKSPACE, 'dev/Tools/build/JenkinsScripts/distribution/scrubbing/validator.py')
|
||||
args = ['python', validator_path, '-p', validator_platform, os.path.join(SCRUBBING_WORKSPACE, 'dev')]
|
||||
if safe_execute_system_call(args):
|
||||
success = False
|
||||
if not success:
|
||||
print 'ERROR: Scrubbing validator failed.'
|
||||
return False
|
||||
print 'Scrubbing validator complete successfully.'
|
||||
return True
|
||||
|
||||
|
||||
def scrubbing_test():
|
||||
if os.path.exists(SCRUBBING_WORKSPACE):
|
||||
os.system('rmdir /s /q \"{}\"'.format(SCRUBBING_WORKSPACE))
|
||||
os.mkdir(SCRUBBING_WORKSPACE)
|
||||
|
||||
client = get_client('s3')
|
||||
zip_name = '{}.zip'.format(JOB_NAME)
|
||||
# Check if zipfile exists in S3 bucket
|
||||
try:
|
||||
client.head_object(Bucket=BUCKET, Key=zip_name)
|
||||
except ClientError as e:
|
||||
if e.response['Error']['Code'] == '404':
|
||||
print 'No previous zipfile found in S3 bucket {}'.format(BUCKET)
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
# Download the zipfile from S3 bucket if the zipfile exists
|
||||
if not s3_download_file(client, SCRUBBING_WORKSPACE, zip_name, BUCKET, max_retry=3):
|
||||
warn('Failed to download {} from S3 bucket {}'.format(zip_name, BUCKET))
|
||||
|
||||
# Unzip the zipfile to SCRUBBING_WORKSPACE
|
||||
zip_path = os.path.join(SCRUBBING_WORKSPACE, zip_name)
|
||||
if os.path.exists(zip_path):
|
||||
zip_file = ZipFile(os.path.join(SCRUBBING_WORKSPACE, zip_name), 'r')
|
||||
zip_file.extractall(SCRUBBING_WORKSPACE)
|
||||
zip_file.close()
|
||||
|
||||
# Copy scrubbing required files to SCRUBBING_WORKSPACE, no overwrite
|
||||
for file in SCRUBBING_REQUIRED_FILES:
|
||||
src_file = os.path.join(WORKSPACE, file)
|
||||
dst_file = os.path.join(SCRUBBING_WORKSPACE, file)
|
||||
copy_file(src_file, dst_file)
|
||||
|
||||
# Get file list and actions that trigger the Jenkins job
|
||||
files = get_files()
|
||||
|
||||
# Copy or delete each file in SCRUBBING_WORKSPACE
|
||||
for f in files:
|
||||
dst_file = os.path.join(SCRUBBING_WORKSPACE, f.path)
|
||||
if 'delete' in f.action:
|
||||
if os.path.exists(dst_file):
|
||||
print 'Deleting {}'.format(dst_file)
|
||||
os.remove(dst_file)
|
||||
else:
|
||||
src_file = os.path.join(WORKSPACE, f.path)
|
||||
copy_file(src_file, dst_file, overwrite=True)
|
||||
|
||||
# Backup the unmodified files and run scrubber and validator
|
||||
backup_path = os.path.join(SCRUBBING_WORKSPACE, 'backup')
|
||||
scrubbing_dev = os.path.join(SCRUBBING_WORKSPACE, 'dev')
|
||||
success = True
|
||||
if os.path.exists(scrubbing_dev):
|
||||
shutil.copytree(scrubbing_dev, os.path.join(backup_path, 'dev'))
|
||||
success = scrub() and validate()
|
||||
|
||||
if success:
|
||||
# Delete zipfile from S3 if validator run successfully
|
||||
try:
|
||||
print 'Deleting {} from bucket {}'.format(zip_name, BUCKET)
|
||||
client.delete_object(Bucket=BUCKET, Key=zip_name)
|
||||
except:
|
||||
warn('Failed to delete {} from bucket {}'.format(zip_name, BUCKET))
|
||||
else:
|
||||
# Upload backup files to S3 bucket
|
||||
if os.path.exists(backup_path):
|
||||
zip_path = os.path.join(SCRUBBING_WORKSPACE, JOB_NAME)
|
||||
shutil.make_archive(zip_path, 'zip', backup_path)
|
||||
if not s3_upload_file(client, SCRUBBING_WORKSPACE, zip_name, BUCKET, max_retry=3):
|
||||
error('Failed to upload {} to S3 bucket {}'.format(zip_name, BUCKET))
|
||||
exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
scrubbing_test()
|
||||
@@ -1,82 +0,0 @@
|
||||
"""
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
'''
|
||||
This script is to update the configuration in dev/bootstrap.cfg
|
||||
Usage: python update_bootstrap_cfg.py --bootstrap_cfg file_path --replace key1=value1,key2=value2
|
||||
'''
|
||||
|
||||
from optparse import OptionParser
|
||||
import os
|
||||
import stat
|
||||
|
||||
|
||||
def update_bootstrap_cfg(file, replace_values):
|
||||
try:
|
||||
with open(file, 'r') as bootstrap_cfg:
|
||||
content = bootstrap_cfg.read()
|
||||
except:
|
||||
error('Cannot read file {}'.format(file))
|
||||
content = content.split('\n')
|
||||
new_content = []
|
||||
for line in content:
|
||||
if not line.startswith('--'):
|
||||
strs = line.split('=')
|
||||
if len(strs):
|
||||
key = strs[0].strip(' ')
|
||||
if key in replace_values:
|
||||
line = '{}={}'.format(key, replace_values[key])
|
||||
new_content.append(line)
|
||||
|
||||
try:
|
||||
with open(file, 'w') as out:
|
||||
out.write('\n'.join(new_content))
|
||||
except:
|
||||
error('Cannot write to file {}'.format(file))
|
||||
print '{} updated with value {}'.format(file, replace_values)
|
||||
|
||||
|
||||
def error(msg):
|
||||
print msg
|
||||
exit(1)
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = OptionParser()
|
||||
parser.add_option("--bootstrap_cfg", dest="bootstrap_cfg", default=None, help="File path of bootstrap.cfg to be updated.")
|
||||
parser.add_option("--replace", dest="replace", default=None, help="Target platform to package")
|
||||
(options, args) = parser.parse_args()
|
||||
bootstrap_cfg = options.bootstrap_cfg
|
||||
replace = options.replace
|
||||
|
||||
if not bootstrap_cfg:
|
||||
error('bootstrap.cfg is not specified.')
|
||||
if not os.path.isfile(bootstrap_cfg):
|
||||
error('File {} not found.'.format(bootstrap_cfg))
|
||||
replace_values = {}
|
||||
if replace:
|
||||
try:
|
||||
replace = replace.split(',')
|
||||
for r in replace:
|
||||
r = r.split('=')
|
||||
key = r[0].strip(' ')
|
||||
value = r[1].strip(' ')
|
||||
replace_values[key] = value
|
||||
except IndexError:
|
||||
error('Please check the format of argument --replace.')
|
||||
|
||||
return bootstrap_cfg, replace_values
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
(file, replace_values) = parse_args()
|
||||
update_bootstrap_cfg(file, replace_values)
|
||||
@@ -1,174 +0,0 @@
|
||||
#!/usr/bin/env 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
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import re
|
||||
import sys
|
||||
import subprocess
|
||||
import zipfile
|
||||
|
||||
'''
|
||||
Creates zip file of <BuildDir>/BenchmarkResults folder and sends the the zip via email to team email
|
||||
'''
|
||||
|
||||
def compute_sha256(input_filepath):
|
||||
'''
|
||||
Computes a SHA-2 hash using a digest that is 256 bits
|
||||
|
||||
Args:
|
||||
input_filepath: File whose content will be hashed using the SHA-2 hash function
|
||||
|
||||
Returns:
|
||||
bytes: byte array containing hash digest in hex
|
||||
'''
|
||||
hasher = hashlib.sha256()
|
||||
hash_result = None
|
||||
|
||||
CHUNK_SIZE = 128 * (1 << 10) # Chunk Size for Sha256 hashing reads file in chunks of 128 KiB
|
||||
with open(input_filepath, 'rb') as hash_file:
|
||||
buf = hash_file.read(CHUNK_SIZE)
|
||||
hasher.update(buf)
|
||||
hash_result = hasher.hexdigest()
|
||||
|
||||
return hash_result
|
||||
|
||||
def create_sha256sums_file(input_filepath):
|
||||
'''
|
||||
Create a sha256sums file from the contents of the input_filepath
|
||||
The sha256sums file will be named using the input_filepath path with an added
|
||||
extension of .sha256sums
|
||||
|
||||
Args:
|
||||
input_filepath: File whose content will be hashed using the SHA-2 hash function
|
||||
|
||||
Returns:
|
||||
string: Path to sha256sums file
|
||||
'''
|
||||
sha256_hash = compute_sha256(input_filepath)
|
||||
if not sha256_hash:
|
||||
print(f'Unable to compute sha256 hash for file {input_filepath}')
|
||||
return None
|
||||
|
||||
hash_filepath = f'{input_filepath}.sha256sums'
|
||||
with open(hash_filepath, "wb") as archive_hash_file:
|
||||
new_hash_contents = f'{sha256_hash} *{os.path.basename(input_filepath)}\n'
|
||||
archive_hash_file.write(new_hash_contents.encode("utf8"))
|
||||
|
||||
return hash_filepath
|
||||
|
||||
def get_files_to_archive(base_dir, regex):
|
||||
'''
|
||||
Gathers list of filepaths to add to archive file
|
||||
Files are looked up with the base directory and cross checked against the supplied regular expression
|
||||
which acts as an inclusion filter
|
||||
|
||||
Args:
|
||||
base_dir: Directory to scan for files
|
||||
regex: Regular expression that is matched against each filename to determine if the file should be
|
||||
added to the archive
|
||||
'''
|
||||
# Get all file names in base directory
|
||||
with os.scandir(base_dir) as dir_entry:
|
||||
filepaths = [pathlib.PurePath(entry.path) for entry in dir_entry if entry.is_file()]
|
||||
# Get all file names matching the regular expression, those file will be added to zip archive
|
||||
archive_files = [str(filepath) for filepath in filepaths if re.match(regex, filepath.as_posix())]
|
||||
return archive_files
|
||||
return None
|
||||
|
||||
|
||||
def create_archive_file(archive_file_prefix, input_filepaths, base_dir):
|
||||
'''
|
||||
Creates a zip file using the supplied input files
|
||||
LZMA compression is used by default for the zip file compression
|
||||
|
||||
Args:
|
||||
archive_file_prefix: Prefix to use as the name of the zip file that should be created
|
||||
input_filepaths: List of input file paths that will be added to zip file
|
||||
base_dir: Directory which is used to create relative paths for each input file path from
|
||||
'''
|
||||
try:
|
||||
zipfile_name = '{}-{:%Y%m%d_%H%M%S}.zip'.format(archive_file_prefix,datetime.now(timezone.utc))
|
||||
# The lzma shared library isn't installed by default on Mac.
|
||||
compression_type = zipfile.ZIP_LZMA if platform.system() != 'Darwin' else zipfile.ZIP_BZIP2
|
||||
with zipfile.ZipFile(zipfile_name, mode='w', compression=compression_type) as benchmark_archive:
|
||||
zipfile_name = benchmark_archive.filename
|
||||
for input_filepath in input_filepaths:
|
||||
# Make input files relative to base_dir when storing them as archived names
|
||||
input_filepath_relpath = os.path.relpath(input_filepath, start=base_dir)
|
||||
benchmark_archive.write(input_filepath, input_filepath_relpath)
|
||||
except OSError as err:
|
||||
print(f'Failed to write benchmark files to zip archive with error {err}')
|
||||
sys.exit(1)
|
||||
except RuntimeError as zip_err:
|
||||
print(f'Runtime Error in zipfile module {zip_err}')
|
||||
sys.exit(1)
|
||||
return zipfile_name
|
||||
|
||||
def upload_to_s3(upload_script_path, base_dir, path_regex, bucket, key_prefix):
|
||||
'''
|
||||
Uploads files which located within the base directory using the upload_to_s3.py script
|
||||
|
||||
Args:
|
||||
base_dir: The directory to pass as the --base-dir value to the upload_to_s3.py script
|
||||
path_regex: The regular expression to pass to the upload_to_s3.py script --file-regex parameter
|
||||
bucket: The s3 bucket to use for the --bucket argument for upload_to_s3.py
|
||||
key_prefix: The prefix to store the uploaded files to within the s3 bucket,
|
||||
It is passed --key-prefix argument to upload_to_s3.py
|
||||
'''
|
||||
try:
|
||||
subprocess.run(['python', upload_script_path, '--base_dir',
|
||||
base_dir, '--file_regex', path_regex,
|
||||
'--bucket', bucket, '--key_prefix', key_prefix],
|
||||
check=True)
|
||||
except subprocess.CalledProcessError as err:
|
||||
print(f'{upload_script_path} failed with error {err}')
|
||||
sys.exit(1)
|
||||
|
||||
def upload_benchmarks(args):
|
||||
'''
|
||||
Main function responsible for determine which files to add to the output zip file and uploading
|
||||
the results to s3
|
||||
|
||||
Args:
|
||||
args: Parse argument list of python command line parameters using the argparse module
|
||||
'''
|
||||
files_to_archive = get_files_to_archive(args.base_dir, args.file_regex)
|
||||
archive_zip_path = create_archive_file(args.output_prefix, files_to_archive, args.base_dir)
|
||||
# Create Sha256sum hash file of zip
|
||||
create_sha256sums_file(archive_zip_path)
|
||||
|
||||
upload_dir = str(pathlib.Path(archive_zip_path).parent)
|
||||
upload_regex = fr'{pathlib.Path(archive_zip_path).name}.*'
|
||||
upload_to_s3(args.upload_to_s3_script_path, upload_dir, upload_regex, args.bucket, args.key_prefix)
|
||||
|
||||
def parse_args():
|
||||
cur_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--base_dir", default=os.getcwd(), help="Base directory to files which should be archived, If not given, then current directory is used.")
|
||||
parser.add_argument("--upload-to-s3-script-path", default=os.path.join(cur_dir, 'upload_to_s3.py'), help="Path to upload_to_s3.py script. Script is used for uploading benchmarks to s3")
|
||||
parser.add_argument("--file_regex", default=r'.*BenchmarkResults/.+\.json', help="Regular expression that used to match file names to archive.")
|
||||
parser.add_argument("-o", "--output-prefix", default='benchmarks_results', help="Prefix to use to construct the name of the zip file where the benchmark results are zipped."
|
||||
" A timestamp will be added to the end of filename")
|
||||
parser.add_argument("--bucket", dest="bucket", default='ly-jenkins-cmake-benchmarks', help="S3 bucket the files are uploaded to.")
|
||||
parser.add_argument("-k", "--key_prefix", default='user_build', dest="key_prefix", help="Object key prefix.")
|
||||
args = parser.parse_args()
|
||||
|
||||
return args
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = parse_args();
|
||||
upload_benchmarks(args)
|
||||
@@ -1,183 +0,0 @@
|
||||
########################################################################################
|
||||
# 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.
|
||||
#
|
||||
#
|
||||
# Original file Copyright Crytek GMBH or its affiliates, used under license.
|
||||
#
|
||||
########################################################################################
|
||||
import ast
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import urllib2
|
||||
import uuid
|
||||
|
||||
KINESIS_STREAM_NAME = 'lumberyard-metrics-stream'
|
||||
KINESIS_MAX_RECORD_SIZE = 1048576 # 1 MB
|
||||
S3_BACKUP_BUCKET = 'infrastructure-build-metrics-backup'
|
||||
IAM_ROLE_NAME = 'ec2-jenkins-node'
|
||||
LOG_FILE_NAME = 'kinesis_upload.log'
|
||||
|
||||
MAX_RECORD_SIZE = KINESIS_MAX_RECORD_SIZE - 4 # to account for version header
|
||||
MAX_RETRIES = 5
|
||||
RETRY_EXCEPTIONS = ('ProvisionedThroughputExceededException',
|
||||
'ThrottlingException')
|
||||
|
||||
# truncate the log file, eventually we need to send the logs to cloudwatch logs
|
||||
with open(LOG_FILE_NAME, 'w'):
|
||||
pass
|
||||
|
||||
logger = logging.getLogger('KinesisUploader')
|
||||
|
||||
fileHdlr = logging.FileHandler(LOG_FILE_NAME)
|
||||
# uncomment this line and the two below to have logs go to stdout for debugging purposes
|
||||
#streamHdlr = logging.StreamHandler(sys.stdout)
|
||||
|
||||
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
|
||||
fileHdlr.setFormatter(formatter)
|
||||
#streamHdlr.setFormatter(formatter)
|
||||
|
||||
logger.addHandler(fileHdlr)
|
||||
#logger.addHandler(streamHdlr)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
def backup_file_to_s3(s3_client, bucket_name, file_location, s3_file_name):
|
||||
try:
|
||||
s3_client.meta.client.upload_file(file_location, bucket_name, s3_file_name)
|
||||
os.remove(file_location)
|
||||
except:
|
||||
logger.error('Failed to upload backup file to S3. This is non-fatal!')
|
||||
# logger.error(traceback.print_exc())
|
||||
|
||||
|
||||
def get_iam_role_credentials(role_name):
|
||||
security_metadata = None
|
||||
try:
|
||||
response = urllib2.urlopen(
|
||||
'http://169.254.169.254/latest/meta-data/iam/security-credentials/{0}'.format(role_name)).read()
|
||||
security_metadata = ast.literal_eval(response)
|
||||
except:
|
||||
logger.error('Unable to get iam role credentials')
|
||||
logger.error(traceback.print_exc())
|
||||
|
||||
return security_metadata
|
||||
|
||||
|
||||
def splitFileByRecord(stream, maxSize):
|
||||
version = 1
|
||||
|
||||
# currently using random GUID for partition key, but in the future we may want to partition by some build id
|
||||
# or by build host
|
||||
partition_key = uuid.uuid4()
|
||||
|
||||
entry_size = 0
|
||||
put_entries = []
|
||||
|
||||
current_entry = ''
|
||||
for line in stream:
|
||||
line_size_in_bytes = len(line.encode('utf-8'))
|
||||
entry_size = entry_size + line_size_in_bytes
|
||||
|
||||
if (entry_size > MAX_RECORD_SIZE):
|
||||
put_entries.append({
|
||||
'Data': str(version) + '\n' + str(current_entry),
|
||||
'PartitionKey': str(partition_key)
|
||||
})
|
||||
|
||||
current_entry = line
|
||||
entry_size = line_size_in_bytes
|
||||
else:
|
||||
current_entry = current_entry + line
|
||||
|
||||
if current_entry:
|
||||
put_entries.append({
|
||||
'Data': str(version) + '\n' + str(current_entry),
|
||||
'PartitionKey': str(partition_key)
|
||||
})
|
||||
|
||||
return put_entries
|
||||
|
||||
|
||||
def main():
|
||||
credentials = get_iam_role_credentials(IAM_ROLE_NAME)
|
||||
|
||||
aws_access_key_id = None
|
||||
aws_secret_access_key = None
|
||||
aws_session_token = None
|
||||
|
||||
if credentials is not None:
|
||||
keys = ['AccessKeyId', 'SecretAccessKey', 'Token']
|
||||
for key in keys:
|
||||
if key not in credentials:
|
||||
logger.error('Unable to find {0} in get_iam_role_credentials response {1}'.format(key, credentials))
|
||||
return
|
||||
|
||||
aws_access_key_id = credentials['AccessKeyId']
|
||||
aws_secret_access_key = credentials['SecretAccessKey']
|
||||
aws_session_token = credentials['Token']
|
||||
|
||||
kinesis_client = boto3.client('kinesis', region_name='us-west-2', aws_access_key_id=aws_access_key_id,
|
||||
aws_secret_access_key=aws_secret_access_key, aws_session_token=aws_session_token)
|
||||
s3_client = boto3.resource('s3', aws_access_key_id=aws_access_key_id, aws_secret_access_key=aws_secret_access_key,
|
||||
aws_session_token=aws_session_token)
|
||||
|
||||
file_location = sys.argv[1]
|
||||
filename = os.path.basename(file_location)
|
||||
backup_file_location = file_location + '.bak'
|
||||
|
||||
try:
|
||||
if os.path.isfile(backup_file_location):
|
||||
logger.info('Found pre-existing backup file. Uploading to S3.')
|
||||
backup_file_to_s3(s3_client, S3_BACKUP_BUCKET, backup_file_location,
|
||||
'{0}.{1}'.format(filename, datetime.now().isoformat()))
|
||||
|
||||
if os.path.isfile(file_location):
|
||||
shutil.copyfile(file_location, backup_file_location)
|
||||
backup_file_to_s3(s3_client, S3_BACKUP_BUCKET, backup_file_location,
|
||||
'{0}.{1}'.format(filename, datetime.now().isoformat()))
|
||||
|
||||
logger.info('Opening metrics file {0}'.format(file_location))
|
||||
with open(file_location, 'r+') as f:
|
||||
records = splitFileByRecord(f, MAX_RECORD_SIZE)
|
||||
i = 0
|
||||
retries = 0
|
||||
while i < len(records):
|
||||
record = records[i]
|
||||
try:
|
||||
logger.info('Uploading {0} bytes of metrics to Kinesis...'.format(len(record)))
|
||||
kinesis_client.put_record(StreamName=KINESIS_STREAM_NAME,
|
||||
Data=record['Data'],
|
||||
PartitionKey=record['PartitionKey'])
|
||||
retries = 0
|
||||
except ClientError as ex:
|
||||
if ex.response['Error']['Code'] not in RETRY_EXCEPTIONS:
|
||||
raise
|
||||
|
||||
sleep_time = 2 ** retries
|
||||
logger.warn('Request throttled by Kinesis, '
|
||||
'sleeping and retrying in {0} seconds'.format(2 ** retries))
|
||||
time.sleep(sleep_time)
|
||||
retries += 1
|
||||
i -= 1
|
||||
|
||||
i += 1
|
||||
|
||||
f.truncate(0)
|
||||
except:
|
||||
logger.error(traceback.print_exc())
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,107 +0,0 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
'''
|
||||
Usage:
|
||||
Use EC2 role to upload all .zip and .MD5 files in %WORKSPACE% folder to bucket ly-packages-mainline:
|
||||
python upload_to_s3.py --base_dir %WORKSPACE% --file_regex "(.*zip$|.*MD5$)" --bucket ly-packages-mainline
|
||||
|
||||
Use profile to upload all .zip and .MD5 files in %WORKSPACE% folder to bucket ly-packages-mainline:
|
||||
python upload_to_s3.py --base_dir %WORKSPACE% --profile profile --file_regex "(.*zip$|.*MD5$)" --bucket ly-packages-mainline
|
||||
|
||||
'''
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import boto3
|
||||
from optparse import OptionParser
|
||||
from util import error
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = OptionParser()
|
||||
parser.add_option("--base_dir", dest="base_dir", default=os.getcwd(), help="Base directory to upload files, If not given, then current directory is used.")
|
||||
parser.add_option("--file_regex", dest="file_regex", default=None, help="Regular expression that used to match file names to upload.")
|
||||
parser.add_option("--profile", dest="profile", default=None, help="The name of a profile to use. If not given, then the default profile is used.")
|
||||
parser.add_option("--bucket", dest="bucket", default=None, help="S3 bucket the files are uploaded to.")
|
||||
parser.add_option("--key_prefix", dest="key_prefix", default='', help="Object key prefix.")
|
||||
'''
|
||||
ExtraArgs used to call s3.upload_file(), should be in json format. extra_args key must be one of: ACL, CacheControl, ContentDisposition, ContentEncoding, ContentLanguage, ContentType, Expires,
|
||||
GrantFullControl, GrantRead, GrantReadACP, GrantWriteACP, Metadata, RequestPayer, ServerSideEncryption, StorageClass,
|
||||
SSECustomerAlgorithm, SSECustomerKey, SSECustomerKeyMD5, SSEKMSKeyId, WebsiteRedirectLocation
|
||||
'''
|
||||
parser.add_option("--extra_args", dest="extra_args", default=None, help="Additional parameters used to upload file.")
|
||||
parser.add_option("--max_retry", dest="max_retry", default=1, help="Maximum retry times to upload file.")
|
||||
(options, args) = parser.parse_args()
|
||||
if not os.path.isdir(options.base_dir):
|
||||
error('{} is not a valid directory'.format(options.base_dir))
|
||||
if not options.file_regex:
|
||||
error('Use --file_regex to specify regular expression that used to match file names to upload.')
|
||||
if not options.bucket:
|
||||
error('Use --bucket to specify bucket that the files are uploaded to.')
|
||||
return options
|
||||
|
||||
|
||||
def get_client(service_name, profile_name):
|
||||
session = boto3.session.Session(profile_name=profile_name)
|
||||
client = session.client(service_name)
|
||||
return client
|
||||
|
||||
|
||||
def get_files_to_upload(base_dir, regex):
|
||||
# Get all file names in base directory
|
||||
files = [x for x in os.listdir(base_dir) if os.path.isfile(os.path.join(base_dir, x))]
|
||||
# Get all file names matching the regular expression, those file will be uploaded to S3
|
||||
files_to_upload = [x for x in files if re.match(regex, x)]
|
||||
return files_to_upload
|
||||
|
||||
|
||||
def s3_upload_file(client, base_dir, file, bucket, key_prefix=None, extra_args=None, max_retry=1):
|
||||
print('Uploading file {} to bucket {}.'.format(file, bucket))
|
||||
key = file if key_prefix is None else '{}/{}'.format(key_prefix, file)
|
||||
for x in range(max_retry):
|
||||
try:
|
||||
client.upload_file(
|
||||
os.path.join(base_dir, file), bucket, key,
|
||||
ExtraArgs=extra_args
|
||||
)
|
||||
print('Upload succeeded')
|
||||
return True
|
||||
except Exception as err:
|
||||
print('exception while uploading: {}'.format(err))
|
||||
print('Retrying upload...')
|
||||
print('Upload failed')
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
options = parse_args()
|
||||
client = get_client('s3', options.profile)
|
||||
files_to_upload = get_files_to_upload(options.base_dir, options.file_regex)
|
||||
extra_args = json.loads(options.extra_args) if options.extra_args else None
|
||||
|
||||
print('Uploading {} files to bucket {}.'.format(len(files_to_upload), options.bucket))
|
||||
failure = []
|
||||
success = []
|
||||
for file in files_to_upload:
|
||||
if not s3_upload_file(client, options.base_dir, file, options.bucket, options.key_prefix, extra_args, 2):
|
||||
failure.append(file)
|
||||
else:
|
||||
success.append(file)
|
||||
print('Upload finished.')
|
||||
print('{} files are uploaded successfully:'.format(len(success)))
|
||||
print('\n'.join(success))
|
||||
if len(failure) > 0:
|
||||
print('{} files failed to upload:'.format(len(failure)))
|
||||
print('\n'.join(failure))
|
||||
# Exit with error code 1 if any file is failed to upload
|
||||
exit(1)
|
||||
@@ -1,65 +0,0 @@
|
||||
"""
|
||||
|
||||
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 json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
|
||||
class LyBuildError(Exception):
|
||||
def __init__(self, message):
|
||||
super(LyBuildError, self).__init__(message)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.message)
|
||||
|
||||
|
||||
def ly_build_error(message):
|
||||
raise LyBuildError(message)
|
||||
|
||||
|
||||
def error(message):
|
||||
print('Error: {}'.format(message))
|
||||
exit(1)
|
||||
|
||||
|
||||
# Exit with status code 0 means it won't fail the whole build process
|
||||
def safe_exit_with_error(message):
|
||||
print('Error: {}'.format(message))
|
||||
exit(0)
|
||||
|
||||
|
||||
def warn(message):
|
||||
print('Warning: {}'.format(message))
|
||||
|
||||
|
||||
def execute_system_call(command, **kwargs):
|
||||
print('Executing subprocess.check_call({})'.format(command))
|
||||
try:
|
||||
subprocess.check_call(command, **kwargs)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(e.output)
|
||||
error('Executing subprocess.check_call({}) failed with error {}'.format(command, e))
|
||||
except FileNotFoundError as e:
|
||||
error("File Not Found - Failed to call {} with error {}".format(command, e))
|
||||
|
||||
|
||||
def safe_execute_system_call(command, **kwargs):
|
||||
print('Executing subprocess.check_call({})'.format(command))
|
||||
try:
|
||||
subprocess.check_call(command, **kwargs)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(e.output)
|
||||
warn('Executing subprocess.check_call({}) failed'.format(command))
|
||||
return e.returncode
|
||||
return 0
|
||||
@@ -1,64 +0,0 @@
|
||||
"""
|
||||
|
||||
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 hashlib
|
||||
import re
|
||||
|
||||
|
||||
def generateFilesetChecksum(filePaths):
|
||||
filesetHash = hashlib.sha512()
|
||||
for filePath in filePaths:
|
||||
updateHashWithFileChecksum(filePath, filesetHash)
|
||||
return filesetHash
|
||||
|
||||
|
||||
def getChecksumForSingleFile(filePath, openMode='rb'):
|
||||
filesetHash = hashlib.sha512()
|
||||
updateHashWithFileChecksum(filePath, filesetHash, openMode)
|
||||
return filesetHash
|
||||
|
||||
|
||||
def getMD5ChecksumForSingleFile(filePath, openMode='rb'):
|
||||
filesetHash = hashlib.md5()
|
||||
updateHashWithFileChecksum(filePath, filesetHash, openMode)
|
||||
return filesetHash
|
||||
|
||||
|
||||
def updateHashWithFileChecksum(filePath, filesetHash, openMode='rb'):
|
||||
BLOCKSIZE = 65536
|
||||
with open(filePath.strip('\n'), openMode) as file:
|
||||
buf = file.read(BLOCKSIZE)
|
||||
while len(buf) > 0:
|
||||
filesetHash.update(buf)
|
||||
buf = file.read(BLOCKSIZE)
|
||||
|
||||
|
||||
def is_valid_hash_sha1(checksum):
|
||||
# sha1 hashes are 40 hex characters long.
|
||||
if len(checksum) is not 40:
|
||||
return False
|
||||
sha1_re = re.compile("(^[0-9A-Fa-f]{40}$)")
|
||||
result = sha1_re.match(checksum)
|
||||
if not result:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def is_valid_hash_sha512(checksum):
|
||||
# sha512 hashes are 128 hex characters long.
|
||||
if len(checksum) is not 128:
|
||||
return False
|
||||
sha512_re = re.compile("(^[0-9A-Fa-f]{128}$)")
|
||||
result = sha512_re.match(checksum)
|
||||
if not result:
|
||||
return False
|
||||
return True
|
||||
@@ -1,76 +0,0 @@
|
||||
"""
|
||||
|
||||
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 os.path
|
||||
import boto3
|
||||
try:
|
||||
import urllib.parse as urllib_compat
|
||||
except:
|
||||
import urlparse as urllib_compat
|
||||
|
||||
def getCloudfrontDistPath(uploadURL):
|
||||
pathFromDomainName = urllib_compat.urlparse(uploadURL)[2]
|
||||
# need to remove the first slash, otherwise it will create a nameless directory on S3
|
||||
return pathFromDomainName[1:]
|
||||
|
||||
|
||||
def getCloudfrontDistribution(cloudfrontUrl, awsCredentialProfileName):
|
||||
session = boto3.session.Session(profile_name=awsCredentialProfileName)
|
||||
cloudfront = session.client('cloudfront')
|
||||
distributionList = cloudfront.list_distributions()
|
||||
targetDistId = None
|
||||
for distribution in distributionList["DistributionList"]["Items"]:
|
||||
if distribution["DomainName"] == urllib_compat.urlparse(cloudfrontUrl)[1]:
|
||||
targetDistId = distribution["Id"]
|
||||
pass
|
||||
assert (targetDistId is not None), "No distribution with the domain name {} found.".format(cloudfrontUrl)
|
||||
targetDist = cloudfront.get_distribution(Id=targetDistId)
|
||||
return targetDist
|
||||
|
||||
|
||||
def getBucket(cloudfrontDistribution, awsCredentialProfileName):
|
||||
bucketName = getBucketName(cloudfrontDistribution)
|
||||
session = boto3.session.Session(profile_name=awsCredentialProfileName)
|
||||
s3 = session.resource('s3')
|
||||
return s3.Bucket(bucketName)
|
||||
|
||||
|
||||
def getBucketName(cloudfrontDistribution):
|
||||
s3Info = cloudfrontDistribution["Distribution"]["DistributionConfig"]["Origins"]["Items"][0]
|
||||
bucketDomainName = s3Info["DomainName"]
|
||||
return bucketDomainName.split('.')[0] # first part of the domain name is the bucket name
|
||||
|
||||
|
||||
def buildBucketPath(cloudfrontUrl, cloudfrontDistribution):
|
||||
s3Info = cloudfrontDistribution["Distribution"]["DistributionConfig"]["Origins"]["Items"][0]
|
||||
originPath = s3Info["OriginPath"]
|
||||
bucketPath = None
|
||||
if originPath:
|
||||
# Start originPath after the first character (presumed to be '/') to avoid nameless directory in S3.
|
||||
bucketPath = str.format("{0}/{1}", originPath[1:], getCloudfrontDistPath(cloudfrontUrl))
|
||||
else:
|
||||
bucketPath = getCloudfrontDistPath(cloudfrontUrl)
|
||||
return bucketPath
|
||||
|
||||
|
||||
def uploadFileToCloudfrontURL(absFilePath, cloudfrontBaseUrl, awsCredentialProfileName, overwrite):
|
||||
cloudfrontDist = getCloudfrontDistribution(cloudfrontBaseUrl, awsCredentialProfileName)
|
||||
s3Bucket = getBucket(cloudfrontDist, awsCredentialProfileName)
|
||||
s3BucketPath = buildBucketPath(cloudfrontBaseUrl, cloudfrontDist)
|
||||
targetBucketPath = urllib_compat.urljoin(s3BucketPath, os.path.basename(absFilePath))
|
||||
|
||||
# Check if file already exists in the S3 bucket.
|
||||
file_exists = len(list(s3Bucket.objects.filter(Prefix=targetBucketPath))) > 0
|
||||
if not file_exists or overwrite:
|
||||
s3Bucket.upload_file(absFilePath, targetBucketPath)
|
||||
|
||||
return targetBucketPath
|
||||
@@ -1,12 +0,0 @@
|
||||
"""
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
@@ -1,303 +0,0 @@
|
||||
"""
|
||||
|
||||
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 boto3
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import netaddr
|
||||
import time
|
||||
import urllib2
|
||||
|
||||
# Order of operations
|
||||
# 1) Get list of internal IPs from dogfish
|
||||
# 2) Translate IP ranges into WAF-vaild CIDR Subnets
|
||||
# 3) Get InternalIPWhitelist rule from WAF
|
||||
# 4) Get all list of all IP Sets on that rule
|
||||
# 5) Loop through all IP ranges in all IP sets
|
||||
# a) If the IP address is not in the set of WAF-valid CIDR Subnets, remove it from the IPSet lists
|
||||
# b) If the IP address is in the set of WAF-valid CIDR Subnets, remove it from the WAF-valid CIDR subnet list
|
||||
# 6) Anything left in the WAF-valid CIDR Subnet list needs to be added to IP Sets
|
||||
# a) create a new IP set if necessary (max 1000 ranges per IP Set)
|
||||
# 7) Push updates to WAF
|
||||
|
||||
rule_name = "Internal_IP_Whitelist"
|
||||
ip_set_basename = "Amazon_Internal_IPs"
|
||||
|
||||
# Maximum number of IP descriptors per IP Set
|
||||
max_ranges_per_ip_set = 1000
|
||||
|
||||
# Maximum number of IP descriptors updates per call
|
||||
max_ranges_per_update = 1000
|
||||
|
||||
ip_version = "IPV4"
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""Handle argument parsing and validate destination folder exists before returning argparse args"""
|
||||
parser = argparse.ArgumentParser(description='Use AWS CLI to update the Internal IP Whitelist in dev or prod')
|
||||
|
||||
parser.add_argument('-l', '--list', action='store_true',
|
||||
help="List the WAF Valid IPs that the list of IPs translates to instead of running the update.")
|
||||
parser.add_argument('-p', '--profile', default='default', help="The name of the AWS CLI profile to use.")
|
||||
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def translate_and_apply_to_waf(ip_ranges, args):
|
||||
subnet_list = list()
|
||||
for ip_range in ip_ranges:
|
||||
subnet_list.extend(translate_range_to_subnet_list(ip_range))
|
||||
|
||||
session = boto3.Session(profile_name=args.profile)
|
||||
waf = session.client('waf')
|
||||
change_tokens = apply_updates_to_waf(subnet_list, waf)
|
||||
|
||||
# wait for every change to finish before ending
|
||||
waiting_on_changes = True
|
||||
while waiting_on_changes:
|
||||
change_in_progress = False
|
||||
time.sleep(.3)
|
||||
for token in change_tokens:
|
||||
status = waf.get_change_token_status(ChangeToken=token["ChangeToken"])
|
||||
change_in_progress = change_in_progress and (status == "PENDING")
|
||||
if change_in_progress:
|
||||
break
|
||||
waiting_on_changes = change_in_progress
|
||||
|
||||
return not waiting_on_changes
|
||||
|
||||
|
||||
def apply_updates_to_waf(ip_list, waf):
|
||||
white_list_rule = get_ip_whitelist_rule(waf)
|
||||
ip_set_list, negate_conditions = get_ip_sets_from_rule(white_list_rule, waf)
|
||||
|
||||
ip_to_add = list(ip_list)
|
||||
adds = list()
|
||||
|
||||
change_tokens = list()
|
||||
|
||||
# for each ip set, get the ip ranges in each descriptor and compare against the ip list
|
||||
# remove duplicates from the list to add
|
||||
for ip_set in ip_set_list:
|
||||
for ip_range_descriptor in ip_set["IPSetDescriptors"]:
|
||||
ip = netaddr.IPNetwork(ip_range_descriptor["Value"])
|
||||
# if an ip in the list is already in an IPSet, then remove it from the list of stuff to add
|
||||
if ip in ip_list:
|
||||
ip_to_add.remove(ip)
|
||||
|
||||
# create add operations
|
||||
for ip in ip_to_add:
|
||||
adds.append(
|
||||
{
|
||||
'Action': 'INSERT',
|
||||
'IPSetDescriptor': {
|
||||
'Type': ip_version,
|
||||
'Value': str(ip)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# make delete operations for anything in a set that isn't in the list of IPs, then populate the rest of the list
|
||||
# with adds until we hit max of the IPSet
|
||||
for ip_set in ip_set_list:
|
||||
removes = list()
|
||||
|
||||
for ip_range_descriptor in ip_set["IPSetDescriptors"]:
|
||||
ip = netaddr.IPNetwork(ip_range_descriptor["Value"])
|
||||
if ip not in ip_list:
|
||||
removes.append(
|
||||
{
|
||||
'Action': 'DELETE',
|
||||
'IPSetDescriptor': ip_range_descriptor
|
||||
}
|
||||
)
|
||||
|
||||
# perform the updates to this set
|
||||
perform_updates_to_existing_ipset(ip_set, removes, adds, waf, change_tokens)
|
||||
|
||||
# we've done all the removes, and filled all existing ip_sets with adds. if we have leftovers, we need to make a
|
||||
# new ip_set and add it to the rule
|
||||
if len(adds) > 0:
|
||||
num_ip_sets = len(ip_set_list)
|
||||
rule_updates = list()
|
||||
while len(adds) > 0:
|
||||
create_token = waf.get_change_token()
|
||||
change_tokens.append(create_token)
|
||||
ip_set = waf.create_ip_set(Name="{0}_{1}".format(ip_set_basename, num_ip_sets),
|
||||
ChangeToken=create_token["ChangeToken"])["IPSet"]
|
||||
num_ip_sets += 1
|
||||
ip_set_id = ip_set["IPSetId"]
|
||||
rule_updates.append(
|
||||
{
|
||||
'Action': 'INSERT',
|
||||
'Predicate': {
|
||||
'Negated': negate_conditions,
|
||||
'Type': 'IPMatch',
|
||||
'DataId': ip_set_id
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# populate the new ip_set
|
||||
batch = move_updates_to_batch(adds)
|
||||
update_set_token = waf.get_change_token()
|
||||
change_tokens.append(update_set_token)
|
||||
waf.update_ip_set(IPSetId=ip_set_id,
|
||||
ChangeToken=update_set_token["ChangeToken"],
|
||||
Updates=batch)
|
||||
|
||||
# update the rule with the new ip_lists
|
||||
update_rule_token = waf.get_change_token()
|
||||
change_tokens.append(update_rule_token)
|
||||
waf.update_rule(RuleId=white_list_rule["RuleId"],
|
||||
ChangeToken=update_rule_token["ChangeToken"],
|
||||
Updates=rule_updates)
|
||||
|
||||
return change_tokens
|
||||
|
||||
|
||||
def perform_updates_to_existing_ipset(ip_set, removes, adds, waf, change_tokens):
|
||||
# figure out how many adds can be done on this ip_set after all of the removes
|
||||
space_remaining_in_set = max_ranges_per_ip_set - (len(ip_set["IPSetDescriptors"]) - len(removes))
|
||||
|
||||
# fill the list of updates to perform with removes and adds until the end result is either a full set or
|
||||
# all operations have been performed.
|
||||
updates = list(removes)
|
||||
updates.extend(list(adds[0:space_remaining_in_set]))
|
||||
adds[0:space_remaining_in_set] = []
|
||||
|
||||
# make batches of updates (max per batch = max_ranges_per_update)
|
||||
update_batches = list()
|
||||
while len(updates) > 0:
|
||||
batch = move_updates_to_batch(updates)
|
||||
update_batches.append(batch)
|
||||
|
||||
# submit an update set request for each batch
|
||||
for batch in update_batches:
|
||||
change_token = waf.get_change_token()
|
||||
change_tokens.append(change_token)
|
||||
waf.update_ip_set(IPSetId=ip_set["IPSetId"],
|
||||
ChangeToken=change_token["ChangeToken"],
|
||||
Updates=batch)
|
||||
|
||||
|
||||
def move_updates_to_batch(original_list):
|
||||
items_to_batch = min(len(original_list), max_ranges_per_update)
|
||||
batch = list(original_list[0:items_to_batch])
|
||||
original_list[0:items_to_batch] = []
|
||||
return batch
|
||||
|
||||
|
||||
def get_ip_whitelist_rule(waf_client):
|
||||
rules_list = waf_client.list_rules(Limit=100)
|
||||
|
||||
whitelist_rule_id = None
|
||||
for rule in rules_list["Rules"]:
|
||||
if rule["Name"] == rule_name:
|
||||
whitelist_rule_id = rule["RuleId"]
|
||||
|
||||
if whitelist_rule_id is None:
|
||||
return None
|
||||
|
||||
return waf_client.get_rule(RuleId=whitelist_rule_id)["Rule"]
|
||||
|
||||
|
||||
def get_ip_sets_from_rule(rule, waf_client):
|
||||
ip_sets = list()
|
||||
negate_conditions = False
|
||||
for condition in rule["Predicates"]:
|
||||
if condition["Type"] == 'IPMatch':
|
||||
ip_set_id = condition["DataId"]
|
||||
ip_set = waf_client.get_ip_set(IPSetId=ip_set_id)
|
||||
|
||||
if ip_set is not None and ip_set_basename in ip_set["IPSet"]["Name"]:
|
||||
ip_sets.append(ip_set["IPSet"])
|
||||
else:
|
||||
print "No IPSet with ID {0}".format(ip_set_id)
|
||||
|
||||
negate_conditions = condition["Negated"]
|
||||
|
||||
return ip_sets, negate_conditions
|
||||
|
||||
|
||||
def translate_range_to_subnet_list(ip_range):
|
||||
"""
|
||||
|
||||
:param ip_range: The IP address + CIDR prefix (IPNetwork object)
|
||||
:return: A list of all WAF-valid subnets that compose the given IP range
|
||||
"""
|
||||
prefix = ip_range.prefixlen
|
||||
waf_valid_prefix = find_closest_waf_range(prefix)
|
||||
return list(ip_range.subnet(waf_valid_prefix))
|
||||
|
||||
|
||||
def find_closest_waf_range(cidr_prefix_length):
|
||||
"""
|
||||
AWS WAF only accepts CIDR ranges of /8, /16, /24, /32. figure out what the closest safe range we need to convert to.
|
||||
This will always return a smaller range than what is passed in for security reasons.
|
||||
|
||||
:param cidr_prefix_length: The arbitrary CIDR range to convert to a WAF-valid range
|
||||
:return: The closest WAF-valid range. i.e. if cidr_prefix_length = 18, this fuction will return 24
|
||||
"""
|
||||
multiple = math.trunc(cidr_prefix_length / 8)
|
||||
return (multiple + 1) * 8 # needs to be 1 based to get accurate range
|
||||
|
||||
|
||||
def list_all_waf_valid_subnets(ip_list):
|
||||
num_ips = 0
|
||||
for ip_range in ip_list:
|
||||
subnets = translate_range_to_subnet_list(ip_range)
|
||||
num_ips += len(subnets)
|
||||
for ip in subnets:
|
||||
print ip
|
||||
return num_ips
|
||||
|
||||
|
||||
def main():
|
||||
# IPs taken from https://w.amazon.com/index.php/PublicIPRanges
|
||||
ips = ["207.171.176.0/20", # SEA
|
||||
"205.251.224.0/22",
|
||||
"176.32.120.0/22",
|
||||
"54.240.196.0/24",
|
||||
"54.231.244.0/22",
|
||||
"52.95.52.0/22",
|
||||
"205.251.232.0/22", # PDX
|
||||
"54.240.230.0/23",
|
||||
"54.240.248.0/21",
|
||||
"54.231.160.0/19",
|
||||
"54.239.2.0/23",
|
||||
"54.239.48.0/22",
|
||||
"52.93.12.0/22",
|
||||
"52.94.208.0/21",
|
||||
"52.218.128.0/17",
|
||||
"204.246.160.0/22", # SFO
|
||||
"205.251.228.0/22",
|
||||
"176.32.112.0/21",
|
||||
"54.240.198.0/24",
|
||||
"54.231.232.0/21",
|
||||
"52.219.20.0/22",
|
||||
"52.219.24.0/21"]
|
||||
|
||||
args = parse_args()
|
||||
|
||||
ip_objects = list(netaddr.IPNetwork(ip) for ip in ips)
|
||||
|
||||
if args.list:
|
||||
print list_all_waf_valid_subnets(ip_objects)
|
||||
else:
|
||||
return translate_and_apply_to_waf(ip_objects, args)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:45c80fc02c64747d3eb8fe76d89979b573401b92482c27d8c95bd01df22c525f
|
||||
size 6582
|
||||
@@ -1,289 +0,0 @@
|
||||
#
|
||||
# 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 glob
|
||||
import shutil
|
||||
import InstallerParams
|
||||
from Insignia import *
|
||||
from InstallerArgs import *
|
||||
from InstallerPackaging import *
|
||||
from Light import *
|
||||
from SignTool import *
|
||||
|
||||
|
||||
# Per Installer:
|
||||
# Heat the includes to build the fragment
|
||||
# candle + light to build the installer / merge module / whatever
|
||||
# Collect the results
|
||||
# If Signing:
|
||||
# Copy installer and cabs to a clean folder
|
||||
# Sign the CAB files
|
||||
# insignia the installer to update its cab file references
|
||||
# Sign the installer
|
||||
# For the Bootstrapper:
|
||||
# Generate S3 links or whatever data we need to shove into the final installer.
|
||||
# Generate a WXS file if we need to based on the above data
|
||||
# candle + light generated WXS + pre-built WXS to build the installer (from
|
||||
# the unsigned files or, if signing, the signed files of the installers)
|
||||
# If Signing:
|
||||
# Copy bootstrapper and cabs to a clean folder
|
||||
# Detach the burn engine from the bootstrapper, and sign it
|
||||
# Reattach the burn engine to the bootstrapper, and sign the bootstrapper
|
||||
# After completion, clean up temp files if necessary.
|
||||
|
||||
|
||||
class OperationMode:
|
||||
StepCounting, BuildInstaller = range(2)
|
||||
|
||||
# It's good practice to define an init for a class in Python, but this
|
||||
# class only exists to serve as an enum for the operation mode.
|
||||
# Defining the __init__ with just a pass is a way to stub out this function so
|
||||
# Python IDEs don't get upset that there is a class with no init.
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
args = createArgs()
|
||||
try:
|
||||
params = InstallerParams.InstallerParams(args)
|
||||
validateArgs(args, params)
|
||||
except InstallerParams.InstallerParamError as error:
|
||||
raise Exception('Installer Params failed to be created with error:\n{}'.format(error))
|
||||
|
||||
# Tracking this like this to simplify calls to performStep.
|
||||
# Otherwise, it has to look like this:
|
||||
# def performStep(mode, stepsTaken, maxSteps, message, operation, *operationArgs):
|
||||
# return stepsTaken+1, result
|
||||
# and calls to performStep have to also capture the stepsTaken.
|
||||
stepsTaken = 0
|
||||
maxSteps = 0
|
||||
operationMode = OperationMode.StepCounting
|
||||
|
||||
|
||||
# The goal of authoring this function is to simplify calls in, which reduces friction when using it.
|
||||
# Call this when you want to only call a function during full operational mode, and not during step counting.
|
||||
# This will use the function's name as the message for the printout.
|
||||
def performStep(operation, *operationArgs):
|
||||
return performStepWithMessage(operation.__name__, operation, *operationArgs)
|
||||
|
||||
|
||||
# Call when you want a message that does not match the function's name.
|
||||
def performStepWithMessage(message, operation, *operationArgs):
|
||||
global stepsTaken
|
||||
result = None
|
||||
if operationMode == OperationMode.BuildInstaller:
|
||||
printProgress(message, stepsTaken, maxSteps)
|
||||
result = operation(*operationArgs)
|
||||
stepsTaken += 1
|
||||
return result
|
||||
|
||||
|
||||
# Call when you want to handle branching yourself. Can be used to trigger a branch for the operation mode.
|
||||
def describeStep(message):
|
||||
global stepsTaken
|
||||
if operationMode == OperationMode.BuildInstaller:
|
||||
printProgress(message, stepsTaken, maxSteps)
|
||||
stepsTaken += 1
|
||||
|
||||
|
||||
def buildInstaller():
|
||||
global stepsTaken
|
||||
stepsTaken = 0
|
||||
# CREATE PACKAGES
|
||||
if not params.skipMsiAndCabCreation:
|
||||
performStep(createThirdPartyPackages, args, params)
|
||||
performStep(createDevPackage, args, params)
|
||||
performStep(createRootPackage, args, params)
|
||||
else:
|
||||
describeStep("Skipping Create Packages (MSIs and Cabs) step, re-using existing packages.")
|
||||
|
||||
if not params.doSigning or not args.bootstrapOnly:
|
||||
params.msiFileNameList = performStepWithMessage("Gathering MSIs",
|
||||
get_file_names_in_directory,
|
||||
params.intermediateInstallerPath,
|
||||
".msi")
|
||||
params.cabFileNameList = performStepWithMessage("Gathering CABs",
|
||||
get_file_names_in_directory,
|
||||
params.intermediateInstallerPath,
|
||||
".cab")
|
||||
|
||||
# SIGN CABs AND MSIs
|
||||
if params.doSigning and not args.bootstrapOnly:
|
||||
# we don't want to modify the original metrics exe, so copy it to where the
|
||||
# other clean files are, and update the path to it
|
||||
if params.metricsPath is not params.intermediateInstallerPath:
|
||||
describeStep("Copying metrics to signing path")
|
||||
if operationMode == OperationMode.BuildInstaller:
|
||||
params.metricsPath = params.intermediateInstallerPath
|
||||
safe_shutil_file_copy(params.fullPathToMetrics, os.path.join(params.metricsPath, params.metricsExe))
|
||||
|
||||
# copy the clean cab and msi files to a new directory to sign them.
|
||||
if params.installerPath is not params.intermediateInstallerPath:
|
||||
describeStep("Copying clean MSI, CAB, and EXE files to signing path")
|
||||
if operationMode == OperationMode.BuildInstaller:
|
||||
if os.path.exists(params.installerPath):
|
||||
verbose_print(args.verbose, "Removing old files from signing path.")
|
||||
shutil.rmtree(params.installerPath)
|
||||
shutil.copytree(params.intermediateInstallerPath, params.installerPath)
|
||||
# don't need the wixpdb files in the signing folder, so get rid of them
|
||||
for filepath in glob.glob(os.path.join(params.installerPath, "*.wixpdb")):
|
||||
os.remove(filepath)
|
||||
|
||||
# Sign the Cab files, verify signing was successful
|
||||
performStep(signtoolSignAndVerifyFiles,
|
||||
params.cabFileNameList,
|
||||
params.installerPath,
|
||||
params.intermediateInstallerPath,
|
||||
params.signingType,
|
||||
args.timestampServer,
|
||||
args.verbose)
|
||||
|
||||
# Run Insignia on the MSI files
|
||||
performStep(insigniaMSIs,
|
||||
params.installerPath,
|
||||
args.verbose,
|
||||
params.msiFileNameList)
|
||||
|
||||
# Sign the MSIs, verify signing was successful
|
||||
performStepWithMessage("Signing and verifying MSIs",
|
||||
signtoolSignAndVerifyFiles,
|
||||
params.msiFileNameList,
|
||||
params.installerPath,
|
||||
params.intermediateInstallerPath,
|
||||
params.signingType,
|
||||
args.timestampServer,
|
||||
args.verbose)
|
||||
|
||||
# Sign the Metrics executable
|
||||
performStepWithMessage("Signing and verifying metrics.exe",
|
||||
signtoolSignAndVerifyFile,
|
||||
params.metricsExe,
|
||||
params.installerPath,
|
||||
params.intermediateInstallerPath,
|
||||
params.signingType,
|
||||
args.timestampServer,
|
||||
args.verbose)
|
||||
|
||||
# make sure that the bootstrapper will get the metrics exe from the right place
|
||||
params.metricsPath = params.installerPath
|
||||
|
||||
# CREATE BOOTSTRAP
|
||||
packageNameList = ""
|
||||
if operationMode is OperationMode.BuildInstaller:
|
||||
for msiName in params.msiFileNameList:
|
||||
packageName = os.path.splitext(msiName)[0]
|
||||
packageNameList += '{};'.format(packageName)
|
||||
# Remove the last semi-colon from the list.
|
||||
packageNameList = packageNameList[:-1]
|
||||
|
||||
# CANDLE BOOTSTRAP
|
||||
success = performStep(candleBootstrap,
|
||||
params.bootstrapWixObjDir,
|
||||
params.installerPath,
|
||||
packageNameList,
|
||||
"LumberyardBootstrapper.wxs Redistributables.wxs",
|
||||
args.hostURL,
|
||||
args.verbose,
|
||||
args.lyVersion,
|
||||
params.metricsPath,
|
||||
params.metricsExe,
|
||||
params.pathTo2015Thru2019Redist,
|
||||
params.redist2015Thru2019Exe,
|
||||
create_id('LumberyardBootstrapper', 'BOOTSTRAPPER', args.lyVersion, args.buildId))
|
||||
|
||||
assert (operationMode is not OperationMode.BuildInstaller or success == 0), \
|
||||
"Failed to generate wixobj file for bootstrapper."
|
||||
|
||||
# LIGHT BOOTSTRAP
|
||||
success = performStep(lightBootstrap,
|
||||
params.bootstrapOutputPath,
|
||||
os.path.join(params.bootstrapWixObjDir, "*.wixobj"),
|
||||
args.verbose,
|
||||
args.cabCachePath)
|
||||
|
||||
assert (operationMode is not OperationMode.BuildInstaller or success == 0), \
|
||||
"Failed to generate executable file for bootstrapper."
|
||||
|
||||
# SIGN ENGINE AND BOOTSTRAPPER
|
||||
if params.doSigning:
|
||||
# make sure the c++ redist gets copied from the temp directory to the actual
|
||||
# installer output path with the bootstrapper.
|
||||
if operationMode == OperationMode.BuildInstaller:
|
||||
safe_shutil_file_copy(os.path.join(params.tempBootstrapOutputDir, params.redist2015Thru2019Exe),
|
||||
os.path.join(params.installerPath, params.redist2015Thru2019Exe))
|
||||
|
||||
unsignedBootstrapPath = os.path.join(params.installerPath, params.tempBootstrapName)
|
||||
signingBootstrapPath = os.path.join(params.installerPath, params.bootstrapName)
|
||||
signingEnginePath = os.path.join(params.installerPath, "engine.exe")
|
||||
if operationMode == OperationMode.BuildInstaller:
|
||||
shutil.copy(params.bootstrapOutputPath, unsignedBootstrapPath)
|
||||
|
||||
# copy bootstrapper to installerPath?
|
||||
if operationMode == OperationMode.BuildInstaller and \
|
||||
params.installerPath is not params.intermediateInstallerPath and \
|
||||
os.path.exists(signingEnginePath):
|
||||
os.remove(signingEnginePath)
|
||||
|
||||
# extract the engine from the bootstrapper with Insignia
|
||||
success = performStep(insigniaDetachBurnEngine,
|
||||
unsignedBootstrapPath,
|
||||
signingEnginePath,
|
||||
args.verbose)
|
||||
assert (operationMode is not OperationMode.BuildInstaller or success == 0), \
|
||||
"Failed to detach burn engine from bootstrapper."
|
||||
|
||||
# sign the engine, verify signing was successful
|
||||
performStep(signtoolSignAndVerifyFile,
|
||||
signingEnginePath,
|
||||
params.installerPath,
|
||||
params.intermediateInstallerPath,
|
||||
params.signingType,
|
||||
args.timestampServer,
|
||||
args.verbose)
|
||||
|
||||
# attach the engine back to the bootstrapper with Insignia
|
||||
success = performStep(insigniaAttachBurnEngine,
|
||||
unsignedBootstrapPath,
|
||||
signingEnginePath,
|
||||
signingBootstrapPath,
|
||||
args.verbose)
|
||||
assert (operationMode is not OperationMode.BuildInstaller or success == -1 or success == 0), \
|
||||
"Failed to reattach burn engine to bootstrapper with error {}.".format(success)
|
||||
|
||||
# delete the stray engine file since it has been reattached to the installer
|
||||
if operationMode is OperationMode.BuildInstaller:
|
||||
os.remove(signingEnginePath)
|
||||
os.remove(unsignedBootstrapPath)
|
||||
|
||||
# sign the bootstrapper, verify the signing was successful
|
||||
performStep(signtoolSignAndVerifyFile,
|
||||
signingBootstrapPath,
|
||||
params.installerPath,
|
||||
params.intermediateInstallerPath,
|
||||
params.signingType,
|
||||
args.timestampServer,
|
||||
args.verbose)
|
||||
|
||||
if args.buildId is not None:
|
||||
performStep(create_version_file, args.buildId, params.installerPath)
|
||||
|
||||
if not args.keep:
|
||||
performStep(cleanTempFiles, params)
|
||||
describeStep("All steps completed")
|
||||
|
||||
|
||||
operationMode = OperationMode.StepCounting
|
||||
maxSteps = 0
|
||||
buildInstaller()
|
||||
|
||||
operationMode = OperationMode.BuildInstaller
|
||||
maxSteps = stepsTaken
|
||||
buildInstaller()
|
||||
@@ -1,206 +0,0 @@
|
||||
#
|
||||
# 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 json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import uuid
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
download_url_base = "http://gamedev.amazon.com/lumberyard/releases/"
|
||||
|
||||
|
||||
def set_download_url_base(url):
|
||||
global download_url_base
|
||||
download_url_base = url
|
||||
|
||||
|
||||
def is_url(potential_url):
|
||||
return urlparse(potential_url)[0] == 'https'
|
||||
|
||||
|
||||
def get_package_name(package_path):
|
||||
path_to_file = package_path
|
||||
if is_url(package_path):
|
||||
# index 2 is everything that isn't a parameter in the URL after the high level domain
|
||||
# see https://docs.python.org/2/library/urlparse.html#urlparse.urlparse for more info
|
||||
path_to_file = urlparse(package_path)[2]
|
||||
return os.path.basename(path_to_file)
|
||||
|
||||
|
||||
def get_ly_version_from_package(args, unpacked_location):
|
||||
version = None
|
||||
path_to_default_settings = os.path.join(unpacked_location, 'dev/_WAF_/default_settings.json')
|
||||
verbose_print(args.verbose, 'Searching for Lumberyard version in {}'.format(path_to_default_settings))
|
||||
with open(path_to_default_settings) as default_settings_file:
|
||||
default_settings_data = json.load(default_settings_file)
|
||||
default_settings_file.close()
|
||||
for build_option in default_settings_data['Build Options']:
|
||||
if 'attribute' in build_option and 'default_value' in build_option:
|
||||
if build_option['attribute'] == 'version':
|
||||
version = build_option['default_value']
|
||||
|
||||
if version is None:
|
||||
verbose_print(args.verbose, 'Version not found in package')
|
||||
raise Exception('Version was not available in default settings.json')
|
||||
|
||||
return version
|
||||
|
||||
|
||||
def append_trailing_slash_to_url(url):
|
||||
if not url.endswith(tuple(['/', '\\'])):
|
||||
url += '/'
|
||||
return url
|
||||
|
||||
|
||||
def generate_target_url(base_target_url, version, build_id, suppress_version_in_path, append_build_id):
|
||||
output_url = base_target_url
|
||||
if append_build_id:
|
||||
output_url = append_trailing_slash_to_url(output_url)
|
||||
output_url += build_id
|
||||
if not suppress_version_in_path:
|
||||
output_url = append_trailing_slash_to_url(output_url)
|
||||
output_url += '{}/installer'.format(version)
|
||||
return output_url
|
||||
|
||||
|
||||
# PRODUCT & UPGRADE/PATCH GUID CREATION
|
||||
def create_id(name, seed, version, build_id):
|
||||
"""
|
||||
Generate the Product GUID using the name, the version, and a changelist value.
|
||||
@param name - Name of the product.
|
||||
@param seed - String used to create a unique GUID.
|
||||
@param version - The version of the product in the form "PRODUCT.MAJOR.MINOR.PATCH".
|
||||
@param build_id - An optional identifier for the build to be used in GUID generation.
|
||||
@return - A GUID for this version of the product.
|
||||
"""
|
||||
# Temporary. Replace the download_url_base with wherever we pass in to the public host or host url.
|
||||
uuid_seed = download_url_base + seed + name + version
|
||||
if build_id is not None:
|
||||
uuid_seed += build_id
|
||||
return str(uuid.uuid3(uuid.NAMESPACE_URL, uuid_seed)).upper()
|
||||
# END PRODUCT & UPGRADE/PATCH GUID CREATION
|
||||
|
||||
|
||||
def replace_leading_numbers(source_string):
|
||||
pattern = re.compile(r'^[0-9]')
|
||||
return pattern.sub('N', source_string)
|
||||
|
||||
|
||||
def strip_special_characters(source_string):
|
||||
"""
|
||||
Remove all non-alphanumeric characters from the given sourceString.
|
||||
@return - A new string that is a copy of the original, containing only
|
||||
letters and numbers.
|
||||
"""
|
||||
if source_string.isalnum():
|
||||
return source_string
|
||||
|
||||
pattern = re.compile(r'[\W_]+')
|
||||
return pattern.sub('', source_string)
|
||||
|
||||
|
||||
def check_for_empty_subfolders(package_root, allowed_empty_folders):
|
||||
# find empty folders
|
||||
empty_folders_found = []
|
||||
for root, dirs, files in os.walk(package_root):
|
||||
if dirs == [] and files == []:
|
||||
empty_folders_found.append(os.path.normpath(os.path.relpath(root, package_root)))
|
||||
if allowed_empty_folders is not None:
|
||||
whitelist_root = 'Whitelist'
|
||||
whitelist_folders = []
|
||||
assert (os.path.exists(allowed_empty_folders)), 'The whitelist file specified at {} does not exist.'.format(allowed_empty_folders)
|
||||
with open(allowed_empty_folders, 'r') as source:
|
||||
json_whitelist = json.load(source)
|
||||
try:
|
||||
for allowed_folder in json_whitelist[whitelist_root]:
|
||||
whitelist_folders.append(os.path.normpath(allowed_folder))
|
||||
except KeyError:
|
||||
print('Unknown json root {}, please check the json root specified.'.format(whitelist_root))
|
||||
exit(1)
|
||||
assert (len(whitelist_folders) > 0), 'The whitelist in the file specified at {} is empty. Either populate the list or omit the argument.'.format(allowed_empty_folders)
|
||||
for folder in empty_folders_found:
|
||||
assert (folder in whitelist_folders), 'The empty folder {} could not be found in the whitelist of empty folders.'.format(folder)
|
||||
|
||||
|
||||
def get_immediate_subdirectories(root_dir):
|
||||
"""
|
||||
Create a list of all directories that exist in the given directory, without
|
||||
recursing through the subdirectories' children.
|
||||
@param root_dir - The directory to search for subdirectories.
|
||||
@return - A list of subdirectories in this directory, excluding their children.
|
||||
"""
|
||||
directories = []
|
||||
for directory in os.listdir(root_dir):
|
||||
if os.path.isdir(os.path.join(root_dir, directory)):
|
||||
directories.append(directory)
|
||||
|
||||
return directories
|
||||
|
||||
|
||||
def get_file_names_in_directory(directory, file_extension=None):
|
||||
"""
|
||||
Create a list of all files in a directory. If given a file extension, it
|
||||
will list all files with that extension.
|
||||
@param directory - The directory to gather files from.
|
||||
@param file_extension - (Optional) The type of files to find. Must be a
|
||||
string in the ".extension" format. (Default None)
|
||||
@retun - A list of names of all files in this directory (that match the given
|
||||
extension if provided).
|
||||
"""
|
||||
file_list = []
|
||||
if file_extension:
|
||||
for file in os.listdir(directory):
|
||||
if file.endswith(file_extension):
|
||||
file_list.append(os.path.basename(file))
|
||||
else:
|
||||
for file in os.listdir(directory):
|
||||
file_list.append(os.path.basename(file))
|
||||
|
||||
return file_list
|
||||
|
||||
|
||||
# VERBOSE RELATED FUNCTIONS
|
||||
|
||||
def verbose_print(isVerbose, message):
|
||||
if isVerbose:
|
||||
print(message)
|
||||
|
||||
|
||||
def find_file_in_package(packageRoot, fileToFind, pathFilters=None):
|
||||
"""
|
||||
Searchs the package path for a file.
|
||||
@param packageRoot: Path to the root of content.
|
||||
@param fileToFind: Name of the file to find.
|
||||
@param pathFilters: Path filter to apply to find.
|
||||
@return: The full path to the file if found, otherwise None.
|
||||
"""
|
||||
for root, dirs, files in os.walk(packageRoot):
|
||||
if fileToFind in files:
|
||||
if pathFilters is None or any(pathFilter in root for pathFilter in pathFilters):
|
||||
return os.path.join(root, fileToFind)
|
||||
return None
|
||||
|
||||
|
||||
def safe_shutil_file_copy(src, dst):
|
||||
# need to remove the old version of dst if it already exists due to a bug
|
||||
# in shutil.copy that causes both the src and dst files to be zeroed out
|
||||
# if they are identical files.
|
||||
if os.path.exists(dst):
|
||||
os.remove(dst)
|
||||
shutil.copy(src, dst)
|
||||
|
||||
|
||||
def create_version_file(buildId, installerPath):
|
||||
with open(os.path.join(installerPath, 'version.txt'), 'w') as versionFile:
|
||||
versionFile.write(buildId)
|
||||
@@ -1,85 +0,0 @@
|
||||
#
|
||||
# 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 glob
|
||||
import os
|
||||
import shutil
|
||||
from BuildInstallerUtils import *
|
||||
|
||||
|
||||
def boolToWixBool(value):
|
||||
"""
|
||||
WiX uses strings "yes" and "no" for their boolean values.
|
||||
@return - "yes" if value == True, otherwise "no".
|
||||
"""
|
||||
if value:
|
||||
return "yes"
|
||||
else:
|
||||
return "no"
|
||||
|
||||
|
||||
def createPackageInfo(directoryName, rootDirectory, outputDirectory, outputPrefix=""):
|
||||
"""
|
||||
Generate commonly used information about a package that is used for WiX functions.
|
||||
@param directoryName - The name of the directory that will be packaged.
|
||||
@param rootDirectory - The path to the given package.
|
||||
@param outputDirectory - The full path (including file name) to the wxs file
|
||||
generated for this package.
|
||||
@param outputPrefix - A string to append to the beginning of the name of the
|
||||
package when creating the output file. (Default = "").
|
||||
@return - A dictionary containing the package name, source information, and
|
||||
output information.
|
||||
"""
|
||||
safeDirectoryName = directoryName
|
||||
if not safeDirectoryName:
|
||||
safeDirectoryName = "packageRoot"
|
||||
moduleName = strip_special_characters(safeDirectoryName)
|
||||
sourceDirectory = os.path.join(rootDirectory, directoryName)
|
||||
wxsName = '{}{}'.format(outputPrefix, moduleName)
|
||||
outputPath = os.path.join(outputDirectory, '{}.wxs'.format(wxsName))
|
||||
componentGroupRef = '{}CG'.format(replace_leading_numbers(wxsName))
|
||||
|
||||
packageInfo = {
|
||||
'name': moduleName,
|
||||
'wxsName': wxsName,
|
||||
'wxsPath': outputPath,
|
||||
'sourceName': safeDirectoryName,
|
||||
'sourcePath': sourceDirectory,
|
||||
'componentGroupRefs': componentGroupRef
|
||||
}
|
||||
return packageInfo
|
||||
|
||||
|
||||
def getVerboseCommand(verboseMode):
|
||||
if verboseMode:
|
||||
return " -v"
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def printProgress(message, stepCount, maxSteps):
|
||||
# We want the step count to line up at the end to the total steps, so add one.
|
||||
print('{}/{}: {}'.format(stepCount + 1, maxSteps, message))
|
||||
|
||||
|
||||
def cleanTempFiles(params):
|
||||
dirs_to_delete = [
|
||||
params.wxsRoot,
|
||||
params.wixObjOutput,
|
||||
params.bootstrapWixObjDir,
|
||||
params.tempBootstrapOutputDir,
|
||||
]
|
||||
|
||||
for del_dir in dirs_to_delete:
|
||||
if os.path.exists(del_dir):
|
||||
shutil.rmtree(del_dir)
|
||||
for filepath in glob.glob(os.path.join(params.intermediateInstallerPath, "*.wixpdb")):
|
||||
os.remove(filepath)
|
||||
@@ -1,145 +0,0 @@
|
||||
#
|
||||
# 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 os
|
||||
from BuildInstallerWixUtils import *
|
||||
|
||||
# CANDLE COMMANDLINE TEMPLATES
|
||||
candleCommandBase = "candle.exe -nologo -o {outputDirectory} {verbose} {preprocessorParams} {wxsFile}"
|
||||
candlePackagePreprocessor = " -dProductGUID={productGUID} -dProductUpgradeGUID={upgradeCodeGUID}" \
|
||||
" -dLumberyardVersion={lumberyardVersion}" \
|
||||
" -dCabPrefix={cabPrefix}" \
|
||||
" -dComponentGroupRefs={packageComponentGroup}" \
|
||||
' -dComponentRefs={componentRefs} -dPackageName="{packageName}"'
|
||||
candleDownloadPreprocessor = " -dROOTURL={downloadURL}"
|
||||
candleBootstrapPreprocessor = "-dLYInstallerPath={installerPath} -dLYInstallerNameList={installerList}" \
|
||||
" -dLumberyardVersion={lumberyardVersion}" \
|
||||
" -dUseStreaming={useStreaming}" \
|
||||
' -dMetricsSourcePath="{metricsSourcePath}"' \
|
||||
' -dMetricsExeName="{metricsExe}"' \
|
||||
' -dPathTo2015Thru2019Redist="{pathTo2015Thru2019Redist}"' \
|
||||
' -dFilename2015Thru2019Redist="{redist2015Thru2019Exe}"' \
|
||||
" -dUpgradeCode={upgradeGUID} -ext WixBalExtension -ext WixUtilExtension"
|
||||
usedCabPrefixList = set()
|
||||
|
||||
|
||||
def candlePackageContent(outputDir, wxsPath, verboseMode):
|
||||
verboseCmd = getVerboseCommand(verboseMode)
|
||||
candleCommand = candleCommandBase.format(outputDirectory=os.path.join(outputDir, ''),
|
||||
verbose=verboseCmd, preprocessorParams="", wxsFile=wxsPath)
|
||||
|
||||
verbose_print(verboseMode, '\n{}\n'.format(candleCommand))
|
||||
return os.system(candleCommand)
|
||||
|
||||
|
||||
def candleAllPackagesContent(packageInfoMap, wixobjDir, verboseMode):
|
||||
for packageInfo in packageInfoMap.values():
|
||||
outputDir = os.path.join(wixobjDir, packageInfo['wxsName'])
|
||||
success = candlePackageContent(outputDir, packageInfo['wxsPath'], verboseMode)
|
||||
assert (success == 0), 'Failed to generate wixobj file for {} content.'.format(packageInfo['name'])
|
||||
|
||||
|
||||
# Cab prefixes have to be less than 8 characters, including the sequential numbers for multiple cabs.
|
||||
# To give room for multiple cabs for an MSI, we're dropping down to 5 characters.
|
||||
# When we strip some paths in 3rd party to 5 characters, they collide, so we do a little extra
|
||||
# Logic to help with collisions.
|
||||
def generateCabPrefix(packageName):
|
||||
# We want a cab prefix length of five to give room for 100+ cabs for an MSI
|
||||
cabPrefixLength = 5
|
||||
|
||||
uniquenessIndex = -1
|
||||
uniquenessValue = 0
|
||||
cabName = packageName[:cabPrefixLength]
|
||||
|
||||
# If there is a cab name collision when two packages truncate to the same five digit
|
||||
# value, then we want to fiddle with the cab prefix to get something unique.
|
||||
# This simple logic starts at the last character in the prefix and replaces it with a numeral
|
||||
# it keeps trying that, and moving earlier in the string as it does so.
|
||||
while cabName in usedCabPrefixList:
|
||||
cabName = cabName[:cabPrefixLength+uniquenessIndex] + str(uniquenessValue) + cabName[cabPrefixLength+uniquenessIndex+1:]
|
||||
uniquenessValue += 1
|
||||
if uniquenessValue > 9:
|
||||
uniquenessValue = 0
|
||||
uniquenessIndex -= 1
|
||||
if uniquenessIndex <= -cabPrefixLength:
|
||||
raise Exception("Could not generate unique cab name for {}".format(packageName))
|
||||
|
||||
usedCabPrefixList.add(cabName)
|
||||
return cabName
|
||||
|
||||
|
||||
def candlePackage(outputDir, wxsTemplatePath, packageInfo, verboseMode, lyVersion, buildId):
|
||||
verboseCmd = getVerboseCommand(verboseMode)
|
||||
|
||||
cabPrefix = generateCabPrefix(packageInfo['name'])
|
||||
productGUID = create_id(packageInfo['name'], 'PRODUCT', lyVersion, buildId)
|
||||
productUpgradeGUID = create_id(packageInfo['name'], 'PRODUCTUPDATE', lyVersion, buildId)
|
||||
componentRefs = packageInfo.get('componentRefs', '')
|
||||
|
||||
candlePreprocessor = candlePackagePreprocessor.format(productGUID=productGUID,
|
||||
upgradeCodeGUID=productUpgradeGUID,
|
||||
lumberyardVersion=lyVersion,
|
||||
componentRefs=componentRefs,
|
||||
cabPrefix=cabPrefix,
|
||||
packageComponentGroup=packageInfo['componentGroupRefs'],
|
||||
packageName=packageInfo['sourceName'])
|
||||
|
||||
candleCommand = candleCommandBase.format(outputDirectory=outputDir,
|
||||
verbose=verboseCmd,
|
||||
preprocessorParams=candlePreprocessor,
|
||||
wxsFile=wxsTemplatePath)
|
||||
|
||||
verbose_print(verboseMode, '\n{}\n'.format(candleCommand))
|
||||
return os.system(candleCommand)
|
||||
|
||||
|
||||
def candlePackages(packageInfoMap, wixobjDir, wxsTemplatePath, verboseMode, lyVersion, buildId):
|
||||
for packageInfo in packageInfoMap.values():
|
||||
outputDir = os.path.join(wixobjDir, packageInfo['wxsName'], 'HeatPackage{}.wixobj'.format(packageInfo['wxsName']))
|
||||
success = candlePackage(outputDir, wxsTemplatePath, packageInfo, verboseMode, lyVersion, buildId)
|
||||
assert (success == 0), 'Failed to generate wixobj file for {}.'.format(packageInfo['name'])
|
||||
|
||||
|
||||
def candleBootstrap(outputDir,
|
||||
installersPath,
|
||||
installerNameList,
|
||||
wxsFileName,
|
||||
downloadURL,
|
||||
verboseMode,
|
||||
lyVersion,
|
||||
metricsSourcePath,
|
||||
metricsExe,
|
||||
pathTo2015Thru2019Redist,
|
||||
redist2015Thru2019Exe,
|
||||
upgradeCodeGUID):
|
||||
verboseCmd = getVerboseCommand(verboseMode)
|
||||
useStreaming = downloadURL is not None
|
||||
useStreamingText = boolToWixBool(useStreaming)
|
||||
candlePreprocessor = candleBootstrapPreprocessor.format(installerPath=os.path.join(installersPath, ''),
|
||||
installerList=installerNameList,
|
||||
lumberyardVersion=lyVersion,
|
||||
useStreaming=useStreamingText,
|
||||
upgradeGUID=upgradeCodeGUID,
|
||||
metricsSourcePath=metricsSourcePath,
|
||||
metricsExe=metricsExe,
|
||||
pathTo2015Thru2019Redist=pathTo2015Thru2019Redist,
|
||||
redist2015Thru2019Exe=redist2015Thru2019Exe)
|
||||
if useStreaming:
|
||||
# The WXS file appends the trailing slash before the package name: "$(var.ROOTURL)/{2}"
|
||||
# We need to strip the trailing slash here if it was passed in with one.
|
||||
downloadURL = downloadURL.rstrip('/')
|
||||
candlePreprocessor += candleDownloadPreprocessor.format(downloadURL=downloadURL)
|
||||
|
||||
candleCommand = candleCommandBase.format(outputDirectory=outputDir, verbose=verboseCmd,
|
||||
preprocessorParams=candlePreprocessor, wxsFile=wxsFileName)
|
||||
|
||||
verbose_print(verboseMode, '\n{}\n'.format(candleCommand))
|
||||
return os.system(candleCommand)
|
||||
@@ -1,257 +0,0 @@
|
||||
#
|
||||
# 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 os
|
||||
import sys
|
||||
import traceback
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from BuildInstallerWixUtils import *
|
||||
|
||||
# HEAT COMMANDLINE TEMPLATES
|
||||
heatCommandBase = 'heat.exe {harvestType} "{harvestSource}" -nologo -sreg -gg'
|
||||
heatCommandRequiredArgs = " -dr {directoryRef} -cg {componentGroup} -out {outputPath}"
|
||||
|
||||
|
||||
def convertToForwardSlashAndLower(pathString):
|
||||
"""
|
||||
Used to ensure path strings are formatted in a consistent manner.
|
||||
"""
|
||||
return pathString.replace('\\', '/').lower()
|
||||
|
||||
|
||||
def lowerDictValues(dictToLower):
|
||||
"""
|
||||
Used to lower-case all values in the given dictionary.
|
||||
"""
|
||||
for key in dictToLower.keys():
|
||||
if isinstance(dictToLower[key], list):
|
||||
dictToLower[key][:] = [convertToForwardSlashAndLower(valueStr) for valueStr in dictToLower[key]]
|
||||
|
||||
|
||||
def removeWxsEntriesWithJsonDirFilelist(wxsFilepath, jsonDirFilelist, directoryKey):
|
||||
"""
|
||||
Removes entries from the given *.wxs file with the given jsonDirFilelist.
|
||||
@param wxsFilepath - Path to *.wxs file to operate the given jsonDirFilelist against
|
||||
@param jsonDirFilelist - Dictionary populated from given "dirFilelist" JSON. Files
|
||||
in the *.wxs files that don't have corresponding entries in dirFilelist will be
|
||||
removed from the *.wxs XML content.
|
||||
"""
|
||||
# Without registering the namespace, the ET XML serialized output
|
||||
# is pretty funky, and WiX will not be happy.
|
||||
namespaceStr = 'http://schemas.microsoft.com/wix/2006/wi'
|
||||
ET.register_namespace('', namespaceStr)
|
||||
tree = ET.ElementTree()
|
||||
tree.parse(wxsFilepath)
|
||||
xmlRoot = tree.getroot()
|
||||
|
||||
# Find all Component tags that have File tags
|
||||
ns = {'wixns': namespaceStr}
|
||||
componentGroupList = xmlRoot.findall('.//wixns:ComponentGroup', ns)
|
||||
for componentGroup in componentGroupList:
|
||||
componentList = componentGroup.findall('.//wixns:Component[wixns:File]', ns)
|
||||
|
||||
for component in componentList:
|
||||
for childFileTag in component:
|
||||
# Entries are typically prefixed with an unnecessary 'SourceDir\' string
|
||||
sourceValue = convertToForwardSlashAndLower(childFileTag.attrib['Source']).replace('sourcedir/', '')
|
||||
if sourceValue not in jsonDirFilelist[directoryKey]:
|
||||
component.remove(childFileTag)
|
||||
|
||||
# WXS files generated by our installer typically only have one File tag per
|
||||
# ComponentGroup, but we'll check that it's empty, just in case.
|
||||
if len(component.getchildren()) < 1:
|
||||
componentGroup.remove(component)
|
||||
|
||||
tree.write(wxsFilepath)
|
||||
|
||||
|
||||
def heatDirectory(wxsName, sourceDirectory, outputPath, componentGroup, directoryRefName, verbose):
|
||||
"""
|
||||
Generate a .wxs file for a given directory, including all subdirectories,
|
||||
and place it at the given outputPath.
|
||||
@remarks - Intentionally hardcoding harvest type to "dir" here. For other
|
||||
types of harvests, a new function should be created. Any logic not
|
||||
directly related to executing the heat command should exist outside this
|
||||
function.
|
||||
"""
|
||||
verboseCmd = getVerboseCommand(verbose)
|
||||
|
||||
# Intentionally hardcoding harvest type to "dir" here. For other types of harvests, a new
|
||||
# function should be created. Any logic not directly related to executing
|
||||
# the heat command should exist outside this function.
|
||||
heatCommand = heatCommandBase.format(harvestType="dir",
|
||||
harvestSource=sourceDirectory)
|
||||
heatCommand += verboseCmd
|
||||
heatCommand += heatCommandRequiredArgs.format(directoryRef=directoryRefName,
|
||||
componentGroup=componentGroup, outputPath=outputPath)
|
||||
|
||||
verbose_print(verbose, '\n{}\n'.format(heatCommand))
|
||||
return os.system(heatCommand)
|
||||
|
||||
|
||||
def heatDirectories(rootDirectory, outputDirectory, directoryRefName, verbose, outputPrefix="", dirFilelist=None):
|
||||
"""
|
||||
Generates package info for every directory in the given rootDirectory, and
|
||||
gathers each package's content information into a .wxs file.
|
||||
@param rootDirectory - The directory containing the source of many packages.
|
||||
@param outputDirectory - The directory to put all generated .wxs files.
|
||||
@param directoryRefName - The ID of the directory for these source files to
|
||||
be placed when installed. (Must match HeatPackageBase directory ID.)
|
||||
@param verbose - Running in verbose mode?
|
||||
@param outputPrefix - A string to append to the beginning of the name of the
|
||||
package when creating the output file. (Default = "").
|
||||
@param dirFilelist - A string that gives a path to a JSON file containing a
|
||||
list of directories, and for each directory, a list of files. Only the
|
||||
files listed in the JSON file will be included in the installer output
|
||||
for the directory specified. This can be used to remove unnecessary
|
||||
files that aren't needed on a customer's machine, but are included in a
|
||||
packaged build created by Jenkins, for example.
|
||||
@return - A dictionary of package names to their associated package information (dictionaries).
|
||||
"""
|
||||
|
||||
# Attempt to parse jsonDirFilelist JSON.
|
||||
jsonDirFilelist = {}
|
||||
if dirFilelist is not None:
|
||||
assert (os.path.exists(dirFilelist)), 'The "dirFilelist" argument was provided but the JSON file at {} does not exist.'.format(dirFilelist)
|
||||
with open(dirFilelist, 'r') as source:
|
||||
try:
|
||||
jsonDirFilelist = json.load(source)
|
||||
except ValueError as e:
|
||||
print(traceback.format_exc())
|
||||
print('Error parsing the given JSON file at {} with exception: {}'.format(dirFilelist, e))
|
||||
sys.exit()
|
||||
except:
|
||||
print(traceback.format_exc())
|
||||
print('Unexpected error parsing the given JSON file at {}. Please verify that the file is correctly formatted.'.format(dirFilelist))
|
||||
sys.exit()
|
||||
|
||||
combinedWxsResults = {}
|
||||
jsonValuesLowered = False
|
||||
|
||||
for directoryName in get_immediate_subdirectories(rootDirectory):
|
||||
packageInfo = createPackageInfo(directoryName, rootDirectory, outputDirectory, outputPrefix)
|
||||
|
||||
moduleName = packageInfo['name']
|
||||
sourceDirectory = packageInfo['sourcePath']
|
||||
wxsName = packageInfo['wxsName']
|
||||
outputPath = packageInfo['wxsPath']
|
||||
# There will only be one component group in the reference list at this point.
|
||||
componentGroup = packageInfo['componentGroupRefs']
|
||||
|
||||
# check for existence of name collision
|
||||
if moduleName in combinedWxsResults:
|
||||
print('ERROR when creating module "{0}" from "{1}". A module with the name "{0}" already exists.'.format(moduleName, sourceDirectory))
|
||||
# Passing let us rapidly iterate on this tool, feel free to upgrade to raising an exception.
|
||||
pass
|
||||
|
||||
combinedWxsResults[moduleName] = packageInfo
|
||||
success = heatDirectory(wxsName, sourceDirectory, outputPath, componentGroup, directoryRefName, verbose)
|
||||
assert (success == 0), 'Failed to generate WXS file for {}.'.format(moduleName)
|
||||
|
||||
sourceDirFormatted = convertToForwardSlashAndLower(sourceDirectory)
|
||||
for key in jsonDirFilelist.keys():
|
||||
if sourceDirFormatted.endswith(convertToForwardSlashAndLower(key)):
|
||||
|
||||
# Lower-case all entries to allow case-insensitive compare
|
||||
if not jsonValuesLowered:
|
||||
lowerDictValues(jsonDirFilelist)
|
||||
jsonValuesLowered = True
|
||||
|
||||
# Alter XML contents of WXS file by filtering it against the JSON
|
||||
# directory list of files.
|
||||
removeWxsEntriesWithJsonDirFilelist(outputPath, jsonDirFilelist, key)
|
||||
|
||||
return combinedWxsResults
|
||||
|
||||
|
||||
def heatFile(file,
|
||||
directoryRefName,
|
||||
rootDirectory,
|
||||
verbose,
|
||||
componentGroup,
|
||||
outputPath):
|
||||
"""
|
||||
Generates a WXS file for an individual file.
|
||||
@param file: Full path to the file to heat.
|
||||
@param directoryRefName: The ID of the directory for these source files to
|
||||
be placed when installed. (Must match HeatPackageBase directory ID.)
|
||||
@param verbose: Running in verbose mode?
|
||||
@param componentGroup: The component group to set in the file.
|
||||
@param outputPath: Where to output the generated WXS file.
|
||||
@return:
|
||||
"""
|
||||
heatCommand = heatCommandBase.format(harvestType="file", harvestSource=file)
|
||||
heatCommand += getVerboseCommand(verbose)
|
||||
# According to Wix's docs ( http://wixtoolset.org/documentation/manual/v3/overview/heat.html )
|
||||
# SRD's description implies that it suppresses root directory harvesting, which seems to imply it works only
|
||||
# in directory harvesting mode. It also implies that it takes in no parameters.
|
||||
# This is either poor documentation, or incorrect (I'm assuming poor documentation).
|
||||
# The actual behavior is:
|
||||
# Normally when harvesting with Heat (directory or file), directories and directory references are generated
|
||||
# based on the path to the root directory. By calling suppress root directory harvesting and passing in a directory,
|
||||
# then the generated nested directory path in the generated wxs file will not include pathing based on this.
|
||||
# This is necessary when harvesting the loose files in a folder's root: Heat's harvesting does not support
|
||||
# whitelist / blacklist functionality, and all subfolders of the package have been harvested in other ways.
|
||||
# This means that all of the loose files in the directory roots that weren't included elsewhere need to be
|
||||
# included in some other installers. If the root directory is not suppressed, then they generate a directory
|
||||
# hierarchy that collides with each other.
|
||||
heatCommand += " -srd " + rootDirectory
|
||||
heatCommand += heatCommandRequiredArgs.format(directoryRef=directoryRefName,
|
||||
componentGroup=componentGroup,
|
||||
outputPath=outputPath)
|
||||
|
||||
verbose_print(verbose, '\n{}\n'.format(heatCommand))
|
||||
return os.system(heatCommand)
|
||||
|
||||
|
||||
def heatFiles(fileList,
|
||||
rootDirectory,
|
||||
outputDirectory,
|
||||
directoryRefName,
|
||||
verbose,
|
||||
outputPrefix = ""):
|
||||
"""
|
||||
Generates WXS files for every file in the file list, and returns a mapping containing the associated
|
||||
WXS files and component groups.
|
||||
@param fileList: The list of files to generate WXS files for.
|
||||
@param outputDirectory: The directory to put generated WXS files.
|
||||
@param directoryRefName: The ID of the directory for these source files to
|
||||
be placed when installed. (Must match HeatPackageBase directory ID.)
|
||||
@param verbose: Running in verbose mode?
|
||||
@param outputPrefix: A string to append to the beginning of the name of the
|
||||
package when creating the output file. (Default = "").
|
||||
@return: A dictionary of input files to their associated WXS information (component group, WXS file location).
|
||||
"""
|
||||
combinedWxsResults = {}
|
||||
for file in fileList:
|
||||
moduleName = strip_special_characters(file)
|
||||
wxsName = '{}{}'.format(outputPrefix, moduleName)
|
||||
outputPath = os.path.join(outputDirectory, '{}.wxs'.format(wxsName))
|
||||
componentGroup = '{}CG'.format(replace_leading_numbers(strip_special_characters(file)))
|
||||
|
||||
wxsInfo = {
|
||||
'name': moduleName,
|
||||
'wxsName': wxsName,
|
||||
'wxsPath': outputPath,
|
||||
'componentGroupRefs': componentGroup
|
||||
}
|
||||
combinedWxsResults[file] = wxsInfo
|
||||
|
||||
success = heatFile(os.path.join(rootDirectory,file),
|
||||
directoryRefName,
|
||||
rootDirectory,
|
||||
verbose,
|
||||
componentGroup,
|
||||
outputPath)
|
||||
assert (success == 0), 'Failed to generate WXS file for {}.'.format(moduleName)
|
||||
|
||||
return combinedWxsResults
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:92fb31b309b613d1f47feb3a728d4528d11d27d9a5e539b109800e209cab0580
|
||||
size 5754
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e708bd9a6e9191431ecc5840e6adaf3f4e5ca52f3f994acefa454bb908476c97
|
||||
size 5435
|
||||
@@ -1,67 +0,0 @@
|
||||
#
|
||||
# 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 os
|
||||
from BuildInstallerWixUtils import *
|
||||
|
||||
# Insignia COMMANDLINE TEMPLATES
|
||||
insigniaCommandBase = "insignia.exe -nologo {verbose} {commandType} {filename} {commandTypeParams}"
|
||||
insigniaDetachEngineParams = "-o {outputPath}"
|
||||
insigniaAttachEngineParams = "{bootstrapName} -o {outputPath}"
|
||||
|
||||
|
||||
def insigniaMSI(filename, verbose):
|
||||
verboseCmd = getVerboseCommand(verbose)
|
||||
insigniaCommand = insigniaCommandBase.format(verbose=verboseCmd,
|
||||
commandType="-im", filename=filename, commandTypeParams="")
|
||||
|
||||
verbose_print(verbose, '\n{}\n'.format(insigniaCommand))
|
||||
return os.system(insigniaCommand)
|
||||
|
||||
|
||||
def insigniaMSIs(directory, verbose, fileList=None):
|
||||
if fileList is None:
|
||||
for file in os.listdir(directory):
|
||||
if file.endswith(".msi"):
|
||||
success = insigniaMSI(file, verbose)
|
||||
assert (success == 0), "Failed to update {} with the signed CAB files' information.".format(os.path.basename(file))
|
||||
else:
|
||||
for file in fileList:
|
||||
filepath = os.path.join(directory, file)
|
||||
success = insigniaMSI(filepath, verbose)
|
||||
assert (success == 0), "Failed to update {} with the signed CAB files' information.".format(os.path.basename(file))
|
||||
|
||||
|
||||
def insigniaDetachBurnEngine(bootstrapName, engineName, verbose):
|
||||
verboseCmd = getVerboseCommand(verbose)
|
||||
|
||||
insigniaParams = insigniaDetachEngineParams.format(outputPath=engineName)
|
||||
insigniaCommand = insigniaCommandBase.format(verbose=verboseCmd,
|
||||
commandType="-ib",
|
||||
filename=bootstrapName,
|
||||
commandTypeParams=insigniaParams)
|
||||
|
||||
verbose_print(verbose, '\n{}\n'.format(insigniaCommand))
|
||||
return os.system(insigniaCommand)
|
||||
|
||||
|
||||
def insigniaAttachBurnEngine(bootstrapName, engineName, outputName, verbose):
|
||||
verboseCmd = getVerboseCommand(verbose)
|
||||
|
||||
insigniaParams = insigniaAttachEngineParams.format(outputPath=outputName,
|
||||
bootstrapName=bootstrapName)
|
||||
insigniaCommand = insigniaCommandBase.format(verbose=verboseCmd,
|
||||
commandType="-ab",
|
||||
filename=engineName,
|
||||
commandTypeParams=insigniaParams)
|
||||
|
||||
verbose_print(verbose, '\n{}\n'.format(insigniaCommand))
|
||||
return os.system(insigniaCommand)
|
||||
@@ -1,115 +0,0 @@
|
||||
#
|
||||
# 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 BuildInstallerUtils
|
||||
from SignTool import *
|
||||
import os
|
||||
import tempfile
|
||||
import ctypes
|
||||
|
||||
|
||||
def createArgs():
|
||||
defaultTempLocation = os.path.join(tempfile.gettempdir(), "LYPackage")
|
||||
|
||||
parser = argparse.ArgumentParser(description='Builds the WiX based Lumberyard Installer for Windows.')
|
||||
# PRIMARY ARGS - many if not all will be used in production
|
||||
# INPUT/OUTPUT
|
||||
parser.add_argument('--packageRoot', default=None, help="Path to the root of the package (default None)")
|
||||
parser.add_argument('--genRoot', default=defaultTempLocation, help="Path for temp data (default Python's temp directory + '/LYPackage')")
|
||||
parser.add_argument('--allowedEmptyFolders', default=None, help="Path to a JSON file containing a list of empty folders that are allowed to exist in --packageRoot")
|
||||
# The default is applied in Build
|
||||
parser.add_argument('--bootstrapName', default=None, help="Bootstrap name (default 'LumberyardInstaller{}.exe'.format(args.lyVersion))")
|
||||
parser.add_argument('--metricsExe', default=None, help="Override path to metrics executable, if not provided this script will search for LyInstallerMetrics.exe in the package root (default None)")
|
||||
# VERSION & GUID
|
||||
parser.add_argument('--lyVersion', default="0.2.2.1", help="Lumberyard Version (default '0.2.2.1')")
|
||||
parser.add_argument('--buildId', default=None, help="The build number of the package that the installer was made from. If not provided, there will be no version file created in the output. (default None)")
|
||||
# SIGNING
|
||||
# more information on the step by step details can be found at:
|
||||
# https://wiki.labcollab.net/confluence/display/lmbr/Sign+Lumberyard+Binaries
|
||||
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('--password', default=None, help="The password for using the signing private key. Must include this if signing should occur. (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('--timestampServer', default="http://tsa.starfieldtech.com", help="The timestamp server to use for signing. (default http://tsa.starfieldtech.com)")
|
||||
# VERBOSE OUTPUT
|
||||
parser.add_argument('-v', '--verbose', action='store_true', help='Enables logging messages (default False)')
|
||||
# URL INFO
|
||||
parser.add_argument('--hostURL', default=None, help='The URL for the installer to download its packages from (msi + cab files). No URL implies the files will be on local disk already. (default None)')
|
||||
# RAPID ITERATION ARGS
|
||||
parser.add_argument('--bootstrapOnly', action='store_true', help="Only create a bootstrapper. Will assume packageRoot contains all necessary MSIs and CABs. Ignores cabCachePath if it was provided.")
|
||||
parser.add_argument('--signOnly', action="store_true", help="Will sign already existing CAB and MSI files. Will then generate a bootstrapper with those signed MSIs, and sign the bootstrapper.")
|
||||
parser.add_argument('--cabCachePath', default=None, help='Path to a cache of the cab files. Should only be used if no new packages are being added, and everything already has cabs built. (default None)')
|
||||
parser.add_argument('-k', '--keep', action='store_true', help="Don't delete temp files")
|
||||
parser.add_argument('--dirFilelist', default=None, help="A list of files (by directory) to include in the install content.")
|
||||
args = parser.parse_args()
|
||||
print("Installer arguments:")
|
||||
print(args)
|
||||
|
||||
# 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."
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def validateArgs(args, params):
|
||||
assert (args.hostURL is not None), "No URL to provide to the bootstrapper was given. Please use --hostURL to specify where the bootstrapper will download the MSI and CAB files from."
|
||||
|
||||
# find empty folders
|
||||
BuildInstallerUtils.check_for_empty_subfolders(args.packageRoot, args.allowedEmptyFolders)
|
||||
|
||||
# BootstrapOnly validation
|
||||
if args.bootstrapOnly:
|
||||
assert (not args.cabCachePath), "Error: Ignoring cabCachePath since bootstrapOnly was specified. Please choose one to use."
|
||||
if args.packageRoot:
|
||||
print("Warning: Specifying packageRoot with bootstrapOnly. Base package not used in bootstrapOnly builds.")
|
||||
# make sure there are MSI files in the installer directory
|
||||
hasInstallerFiles = False
|
||||
for file in os.listdir(params.intermediateInstallerPath):
|
||||
hasInstallerFiles = file.endswith(".msi") or file.endswith(".cab")
|
||||
if hasInstallerFiles:
|
||||
return
|
||||
assert hasInstallerFiles, "The packageRoot given ({}) does not contain any MSI or CAB files. When using bootstrapOnly, packageRoot should point to the directory with MSIs and CAB files.".format(params.intermediateInstallerPath)
|
||||
|
||||
# Make sure metrics and Visual Studio 2015-2019 redist exist somewhere
|
||||
assert (params.fullPathToMetrics is not None), "Metrics executable path was not provided and was not found in the package."
|
||||
assert (os.path.exists(params.fullPathToMetrics)), "Metrics executable expected at {}, but cannot be found.".format(params.fullPathToMetrics)
|
||||
assert (params.fullPathTo2015Thru2019Redist is not None), "Visual Studio 2015-2019 redist could not be found in the package."
|
||||
assert (os.path.exists(params.fullPathTo2015Thru2019Redist)), "Visual Studio 2015-2019 redist expected at {}, but cannot be found.".format(params.fullPathTo2015Thru2019Redist)
|
||||
|
||||
# Signing parameter asserts
|
||||
if params.doSigning:
|
||||
# Installation requires administration privileges.
|
||||
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."
|
||||
|
||||
if args.privateKey is not None:
|
||||
# make sure that private key is valid and we were given a password to use.
|
||||
assert (os.path.exists(args.privateKey)), "No private key exists at the given path."
|
||||
assert (args.privateKey.endswith(".pfx")), "The file at {} is not a signing private key.".format(args.privateKey)
|
||||
assert (args.password is not None), "Must include the password needed for the signing private key to sign."
|
||||
# if --certName is specified, the verification of that name being valid must be done prior to this script being run, as it is platform dependent.
|
||||
|
||||
# Make sure that all CABs and MSI files are signed if only the bootstrapper needs to be built.
|
||||
if args.bootstrapOnly:
|
||||
for filename in params.msiFileNameList:
|
||||
signed = signtoolVerifySign(os.path.join(params.installerPath, filename), args.verbose)
|
||||
assert signed, "Not all MSI and CAB files in {} are verified to have been signed. Please use --signOnly instead.".format(params.installerPath)
|
||||
for filename in params.cabFileNameList:
|
||||
signed = signtoolVerifySign(os.path.join(params.installerPath, filename), args.verbose)
|
||||
assert signed, "Not all MSI and CAB files in {} are verified to have been signed. Please use --signOnly False instead.".format(params.installerPath)
|
||||
signed = signtoolVerifySign(params.fullPathToMetrics, args.verbose)
|
||||
assert signed, "Metrics executable at {} is not verified to have been signed. Please either create a signed executable or use --signOnly instead.".format(params.fullPathToMetrics)
|
||||
|
||||
if args.signOnly:
|
||||
assert params.doSigning, "Cannot specify --signOnly if not given a private key and password, or a certificate name for signing."
|
||||
@@ -1,273 +0,0 @@
|
||||
#
|
||||
# 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()
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:30befe12828ae67d1f3b4a9725c98d910c2190b81caa0f9d919e9c42c432c33a
|
||||
size 101185
|
||||
@@ -1,225 +0,0 @@
|
||||
#
|
||||
# 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 os
|
||||
from Heat import *
|
||||
from Candle import *
|
||||
from Light import *
|
||||
from BuildInstallerWixUtils import *
|
||||
|
||||
|
||||
def createLooseFilePackage(args,
|
||||
params,
|
||||
packageGroupInfoMap,
|
||||
path,
|
||||
wixDirId,
|
||||
directory,
|
||||
wixSafePathId):
|
||||
# DISCOVER LOOSE FILES IN PATH
|
||||
looseFiles = []
|
||||
for (dirpath, dirnames, filenames) in os.walk(path):
|
||||
looseFiles.extend(filenames)
|
||||
# Only the files in the root directory need to
|
||||
# be discovered this way, so break on first result.
|
||||
break
|
||||
|
||||
# If there are no loose files, there is no package to create.
|
||||
if not looseFiles:
|
||||
return False
|
||||
|
||||
# HEAT LOOSE FILES
|
||||
looseFileMap = heatFiles(looseFiles,
|
||||
path,
|
||||
params.wxsRoot,
|
||||
wixDirId,
|
||||
args.verbose,
|
||||
wixSafePathId)
|
||||
|
||||
# CANDLE LOOSE FILES
|
||||
rootInfo = createPackageInfo(directory, args.packageRoot, params.wxsRoot)
|
||||
|
||||
# There is no general component group for loose files, so clear it out.
|
||||
rootInfo['componentGroupRefs'] = ''
|
||||
packageGroupInfoMap[rootInfo['name']] = rootInfo
|
||||
|
||||
candleSubDirectory = directory
|
||||
if not candleSubDirectory:
|
||||
candleSubDirectory = wixSafePathId
|
||||
|
||||
for looseFile in looseFileMap:
|
||||
candlePackageContent(os.path.join(params.wixObjOutput, candleSubDirectory),
|
||||
looseFileMap[looseFile]['wxsPath'],
|
||||
args.verbose)
|
||||
if rootInfo.get('componentGroupRefs', ''):
|
||||
rootInfo['componentGroupRefs'] += ';' + looseFileMap[looseFile]['componentGroupRefs']
|
||||
else:
|
||||
rootInfo['componentGroupRefs'] = looseFileMap[looseFile]['componentGroupRefs']
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def createThirdPartyPackages(args, params):
|
||||
"""
|
||||
Creates a .msi and .cab files for each folder in 3rdParty, and places them at
|
||||
intermediateInstallerPath to be used when creating the bootstrapper.
|
||||
@return - A dictionary of the package information that was generated for the
|
||||
3rd Party directories.
|
||||
"""
|
||||
thirdPartyWixDirId = "THIRDPARTYDIR"
|
||||
thirdPartyPath = os.path.join(args.packageRoot, "3rdParty")
|
||||
# HEAT PACKAGE
|
||||
thirdPartyInfoMap = heatDirectories(thirdPartyPath, params.wxsRoot,
|
||||
thirdPartyWixDirId, args.verbose, "ThirdParty")
|
||||
# CANDLE PACKAGE CONTENTS
|
||||
candleAllPackagesContent(thirdPartyInfoMap, params.wixObjOutput, args.verbose)
|
||||
|
||||
# CREATE PACKAGE INFORMATION FOR LOOSE FILES IN 3RD PARTY ROOT
|
||||
createLooseFilePackage(args,
|
||||
params,
|
||||
thirdPartyInfoMap,
|
||||
thirdPartyPath,
|
||||
thirdPartyWixDirId,
|
||||
"3rdParty",
|
||||
"ThirdParty")
|
||||
|
||||
# CANDLE PACKAGES
|
||||
candlePackages(thirdPartyInfoMap,
|
||||
params.wixObjOutput,
|
||||
params.heatPackageBase,
|
||||
args.verbose,
|
||||
args.lyVersion,
|
||||
args.buildId)
|
||||
|
||||
# LIGHT PACKAGES
|
||||
lightPackages(thirdPartyInfoMap,
|
||||
params.packagesPath,
|
||||
params.wixObjOutput,
|
||||
args.verbose,
|
||||
args.cabCachePath)
|
||||
|
||||
return thirdPartyInfoMap
|
||||
|
||||
|
||||
def createRootPackage(args, params):
|
||||
rootInfoMap = {}
|
||||
if createLooseFilePackage(args,
|
||||
params,
|
||||
rootInfoMap,
|
||||
os.path.join(args.packageRoot, ""),
|
||||
"INSTALLDIR",
|
||||
"",
|
||||
"packageRoot"):
|
||||
# CANDLE PACKAGES
|
||||
candlePackages(rootInfoMap,
|
||||
params.wixObjOutput,
|
||||
params.heatPackageBase,
|
||||
args.verbose,
|
||||
args.lyVersion,
|
||||
args.buildId)
|
||||
rootInfoMap['packageRoot']['sourcePath'] = os.path.abspath(args.packageRoot)
|
||||
# LIGHT PACKAGES
|
||||
lightPackages(rootInfoMap,
|
||||
params.packagesPath,
|
||||
params.wixObjOutput,
|
||||
args.verbose,
|
||||
args.cabCachePath)
|
||||
|
||||
return rootInfoMap
|
||||
|
||||
|
||||
def createDevPackage(args, params):
|
||||
"""
|
||||
Creates a .msi and .cab files for the dev folder, and places them at
|
||||
intermediateInstallerPath to be used when creating the bootstrapper.
|
||||
@return - A dictionary of the package information that was generated for the
|
||||
dev directory.
|
||||
"""
|
||||
devWixDirId = "DEVDIR"
|
||||
devPath = os.path.join(args.packageRoot, "dev")
|
||||
|
||||
# HEAT PACKAGE
|
||||
devInfoMap = heatDirectories(devPath, params.wxsRoot, devWixDirId, args.verbose, "dev", args.dirFilelist)
|
||||
|
||||
# CANDLE PACKAGE CONTENTS
|
||||
candleAllPackagesContent(devInfoMap, params.wixObjOutput, args.verbose)
|
||||
|
||||
# CREATE PACKAGE INFORMATION FOR LOOSE FILES IN DEV ROOT
|
||||
createLooseFilePackage(args,
|
||||
params,
|
||||
devInfoMap,
|
||||
devPath,
|
||||
devWixDirId,
|
||||
"dev",
|
||||
"dev")
|
||||
|
||||
devInfo = devInfoMap["dev"]
|
||||
devInfo['componentRefs'] = 'DesktopShortcuts;StartMenuShortcuts;LevelListRegistryKeys'
|
||||
|
||||
# CANDLE PACKAGE
|
||||
candlePackages(devInfoMap,
|
||||
params.wixObjOutput,
|
||||
params.heatPackageBase,
|
||||
args.verbose,
|
||||
args.lyVersion,
|
||||
args.buildId)
|
||||
|
||||
# Build the dev-specific wxs components (for shortcuts)
|
||||
candlePackage('{}/{}/'.format(params.wixObjOutput, devInfo['name']),
|
||||
'HeatDevPackageBase.wxs',
|
||||
devInfo,
|
||||
args.verbose,
|
||||
args.lyVersion,
|
||||
args.buildId)
|
||||
|
||||
# LIGHT PACKAGE
|
||||
lightPackages(devInfoMap,
|
||||
params.packagesPath,
|
||||
params.wixObjOutput,
|
||||
args.verbose,
|
||||
args.cabCachePath)
|
||||
|
||||
return devInfoMap
|
||||
|
||||
|
||||
def createRootFolderPackage(args, params, folderName):
|
||||
"""
|
||||
Creates a .msi and .cab files for a folder in the package root, like docs, and places them at
|
||||
intermediateInstallerPath to be used when creating the bootstrapper.
|
||||
@return - A dictionary of the package information that was generated for the
|
||||
docs directory.
|
||||
"""
|
||||
folderInfo = createPackageInfo(folderName, args.packageRoot, params.wxsRoot)
|
||||
folderInfoMap = {}
|
||||
folderInfoMap[folderInfo['name']] = folderInfo
|
||||
|
||||
# HEAT PACKAGE
|
||||
heatDirectory(folderInfo['wxsName'], folderInfo['sourcePath'], folderInfo['wxsPath'],
|
||||
folderInfo['componentGroupRefs'], "INSTALLDIR", args.verbose)
|
||||
|
||||
# CANDLE PACKAGE CONTENTS
|
||||
candleAllPackagesContent(folderInfoMap, params.wixObjOutput, args.verbose)
|
||||
|
||||
# CANDLE PACKAGE
|
||||
candlePackages(folderInfoMap,
|
||||
params.wixObjOutput,
|
||||
params.heatPackageBase,
|
||||
args.verbose,
|
||||
args.lyVersion,
|
||||
args.buildId)
|
||||
|
||||
# LIGHT PACKAGE
|
||||
lightPackages(folderInfoMap,
|
||||
params.packagesPath,
|
||||
params.wixObjOutput,
|
||||
args.verbose,
|
||||
args.cabCachePath)
|
||||
|
||||
return folderInfoMap
|
||||
@@ -1,93 +0,0 @@
|
||||
#
|
||||
# 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 os
|
||||
from SignTool import *
|
||||
|
||||
|
||||
class InstallerParamError(Exception):
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
def __str__(self):
|
||||
return repr(self.value)
|
||||
|
||||
|
||||
class InstallerParams(object):
|
||||
def __init__(self, args):
|
||||
self.fullPathTo2015Thru2019Redist = None
|
||||
self.signingType = None
|
||||
|
||||
if args.privateKey is not None:
|
||||
if not args.password:
|
||||
import getpass
|
||||
args.password = getpass.getpass("Please provide the signing password: ")
|
||||
self.signingType = KeySigning(args.privateKey, args.password)
|
||||
elif args.certName is not None:
|
||||
self.signingType = NameSigning(args.certName)
|
||||
|
||||
self.doSigning = self.signingType is not None
|
||||
|
||||
self.wxsRoot = os.path.join(args.genRoot, "wxs")
|
||||
self.wixObjOutput = os.path.join(args.genRoot, "wixobj_module")
|
||||
self.heatPackageBase = "HeatPackageBase.wxs"
|
||||
self.packagesPath = os.path.join(args.genRoot, "unsignedInstaller")
|
||||
# Wix primarily differentiates between a file and directory by looking for a trailing slash.
|
||||
# os.path.join with an empty string is a way to guarantee a trailing slash is added to a path.
|
||||
self.bootstrapWixObjDir = os.path.join(os.path.join(args.genRoot, "wixobj_bootstrap"), '')
|
||||
self.intermediateInstallerPath = os.path.join(args.genRoot, "unsignedInstaller")
|
||||
|
||||
self.installerPath = self.intermediateInstallerPath
|
||||
if self.doSigning:
|
||||
self.installerPath = os.path.join(args.genRoot, "installer")
|
||||
|
||||
self.sourcePath = args.packageRoot
|
||||
if args.bootstrapOnly:
|
||||
self.sourcePath = self.installerPath
|
||||
|
||||
self.fullPathToMetrics = args.metricsExe
|
||||
if self.fullPathToMetrics is None:
|
||||
self.fullPathToMetrics = find_file_in_package(self.sourcePath, "LyInstallerMetrics.exe", ["InternalSDKs"])
|
||||
if self.fullPathToMetrics is None:
|
||||
raise InstallerParamError('Path to LyInstallerMetrics.exe could not be found underneath the Tools/InternalSDKs folder in the package ')
|
||||
|
||||
# Gather information on the Visual Studio 2015-2019 redistributable.
|
||||
# Note that this is a hardcoded path, and not a search. This is because there are multiple different redistributables
|
||||
# with the exact same name, the easiest way to identify which is which is based on the location in the package.
|
||||
# Also, this VS 2015-2019 redistributable appears multiple times in the package. For consistency, we want to make sure
|
||||
# the exact same one is used.
|
||||
self.name2015Thru2019Redist = "VC_redist.x64.exe"
|
||||
self.fullPathTo2015Thru2019Redist = os.path.join(args.packageRoot, "dev", "Tools", "Redistributables", "Visual Studio 2015-2019", self.name2015Thru2019Redist)
|
||||
# When rebuilding just the bootstrap for a package, the source package root is a pointer to where the installer was
|
||||
# generated previously. At this point, the redistributable is already in the package root.
|
||||
if args.bootstrapOnly:
|
||||
self.fullPathTo2015Thru2019Redist = find_file_in_package(self.sourcePath, [self.name2015Thru2019Redist])
|
||||
|
||||
self.skipMsiAndCabCreation = args.signOnly or args.bootstrapOnly
|
||||
self.metricsPath, self.metricsExe = os.path.split(self.fullPathToMetrics)
|
||||
self.pathTo2015Thru2019Redist, self.redist2015Thru2019Exe = os.path.split(self.fullPathTo2015Thru2019Redist)
|
||||
self.tempBootstrapOutputDir = os.path.join(args.genRoot, "buildBootstrap")
|
||||
|
||||
if self.doSigning and args.bootstrapOnly:
|
||||
self.msiFileNameList = get_file_names_in_directory(self.installerPath, ".msi")
|
||||
self.cabFileNameList = get_file_names_in_directory(self.installerPath, ".cab")
|
||||
|
||||
# Default to LumberyardInstallerVERSION.exe, unless args.bootstrapName is set
|
||||
if args.bootstrapName:
|
||||
self.bootstrapName = args.bootstrapName
|
||||
else:
|
||||
self.bootstrapName = "LumberyardInstaller{}.exe".format(args.lyVersion)
|
||||
|
||||
if self.doSigning:
|
||||
self.tempBootstrapName = "temp" + self.bootstrapName
|
||||
self.bootstrapOutputPath = os.path.join(self.tempBootstrapOutputDir, self.tempBootstrapName)
|
||||
else:
|
||||
self.bootstrapOutputPath = os.path.join(self.installerPath, self.bootstrapName)
|
||||
@@ -1,57 +0,0 @@
|
||||
#
|
||||
# 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 os
|
||||
from BuildInstallerWixUtils import *
|
||||
|
||||
# LIGHT COMMANDLINE TEMPLATES
|
||||
lightCommandBase = 'light.exe -nologo -o {outputPath} -ext WixUIExtension -ext WiXUtilExtension -b "{packageSource}" {verbose} {wixobjFile}'
|
||||
lightCommandBootstrap = 'light.exe -nologo -o {outputPath} -ext WixBalExtension -ext WiXUtilExtension {verbose} {wixobjFile}'
|
||||
lightCommandCabCache = " -cc {cabCachePath} -reusecab"
|
||||
|
||||
|
||||
def lightPackage(outputPath, sourceDirectory, wixobjFiles, verbose, cabCachePath):
|
||||
verboseCmd = getVerboseCommand(verbose)
|
||||
lightCommand = lightCommandBase.format(outputPath=outputPath,
|
||||
verbose=verboseCmd,
|
||||
packageSource=sourceDirectory,
|
||||
wixobjFile=wixobjFiles)
|
||||
|
||||
if cabCachePath is not None:
|
||||
lightCommand += lightCommandCabCache.format(cabCachePath=cabCachePath)
|
||||
|
||||
verbose_print(verbose, '\n{}\n'.format(lightCommand))
|
||||
return os.system(lightCommand)
|
||||
|
||||
|
||||
def lightPackages(packageInfoMap, packagesPath, wixobjFiles, verboseMode, cabCachePath):
|
||||
numPackagesBuilt = 0
|
||||
|
||||
for packageInfo in packageInfoMap.values():
|
||||
outputPath = os.path.join(packagesPath, '{}.msi'.format(packageInfo['wxsName']))
|
||||
packageWixObjPath = os.path.join(os.path.join(wixobjFiles, packageInfo['wxsName']), "*.wixobj")
|
||||
|
||||
success = lightPackage(outputPath, packageInfo['sourcePath'], packageWixObjPath, verboseMode, cabCachePath)
|
||||
assert (success == 0), 'Failed to generate msi and cab files for {}.'.format(packageInfo['name'])
|
||||
|
||||
numPackagesBuilt += 1
|
||||
verbose_print(verboseMode, '\nPackages Built: {}\n\n'.format(numPackagesBuilt))
|
||||
|
||||
|
||||
def lightBootstrap(outputPath, wixobjFiles, verbose, cabCachePath):
|
||||
verboseCmd = getVerboseCommand(verbose)
|
||||
lightCommand = lightCommandBootstrap.format(outputPath=outputPath, verbose=verboseCmd, wixobjFile=wixobjFiles)
|
||||
|
||||
if cabCachePath is not None:
|
||||
lightCommand += lightCommandCabCache.format(cabCachePath=cabCachePath)
|
||||
|
||||
verbose_print(verbose, '\n{}\n'.format(lightCommand))
|
||||
return os.system(lightCommand)
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:52fa9d61db7371183e66e422e8f959fd613a6c3e46035a8c1d05a4329445855b
|
||||
size 5303
|
||||
@@ -1,14 +0,0 @@
|
||||
REM
|
||||
REM
|
||||
REM All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
REM its licensors.
|
||||
REM
|
||||
REM For complete copyright and license terms please see the LICENSE at the root of this
|
||||
REM distribution (the "License"). All use of this software is governed by the License,
|
||||
REM or, if provided, by the license below or the license accompanying this file. Do not
|
||||
REM remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
REM
|
||||
|
||||
@ECHO off
|
||||
certutil -user -addstore Root LumberyardDevCA.cer
|
||||
@@ -1,61 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<WixLocalization Culture="en-us" Language="1033" xmlns="http://schemas.microsoft.com/wix/2006/localization">
|
||||
<String Id="Caption">[WixBundleName] PC Setup</String>
|
||||
<String Id="Title">[WixBundleName] PC</String>
|
||||
<String Id="InstallHeader">Welcome</String>
|
||||
<String Id="InstallMessage">This setup tool installs the PC version of [WixBundleName] on Local Disk (C:). To change the install directory, click Options.</String>
|
||||
<String Id="InstallVersion">Version [WixBundleVersion] PC</String>
|
||||
<String Id="ConfirmCancelMessage">Are you sure you want to cancel?</String>
|
||||
<String Id="ExecuteUpgradeRelatedBundleMessage">Previous version</String>
|
||||
<String Id="HelpHeader">Setup Help</String>
|
||||
<String Id="HelpText">/install | /repair | /uninstall | /layout [directory] - installs, repairs, uninstalls or
|
||||
creates a complete local copy of the bundle in directory. Install is the default.
|
||||
|
||||
/passive | /quiet - displays minimal UI with no prompts or displays no UI and
|
||||
no prompts. By default UI and all prompts are displayed.
|
||||
|
||||
/norestart - suppress any attempts to restart. By default UI will prompt before restart.
|
||||
/log log.txt - logs to a specific file. By default a log file is created in %TEMP%.</String>
|
||||
<String Id="HelpCloseButton">&Close</String>
|
||||
<String Id="InstallEulaAgreementText">By installing Lumberyard you agree to the <a href="https://aws.amazon.com/agreement">AWS Customer Agreement</a>, <a href="https://aws.amazon.com/service-terms/#57._Amazon_Lumberyard_Engine">Lumberyard Service Terms</a>, and <a href="https://aws.amazon.com/privacy">Privacy Notice</a>.</String>
|
||||
<String Id="InstallLicenseLinkText">[WixBundleName] <a href="#">license terms</a>.</String>
|
||||
<String Id="InstallAcceptCheckbox">I &agree to the license terms and conditions</String>
|
||||
<String Id="InstallOptionsButton">&Options</String>
|
||||
<String Id="InstallInstallButton">&Install</String>
|
||||
<String Id="InstallCloseButton">&Cancel</String>
|
||||
<String Id="OptionsHeader">Setup options</String>
|
||||
<String Id="OptionsLocationLabel">Install location:</String>
|
||||
<String Id="OptionsBrowseButton">&Browse</String>
|
||||
<String Id="OptionsOkButton">&OK</String>
|
||||
<String Id="OptionsCancelButton">&Cancel</String>
|
||||
<String Id="ProgressHeader">Setup Progress</String>
|
||||
<String Id="DownloadLabel">Acquiring:</String>
|
||||
<String Id="ProgressLabel">Processing:</String>
|
||||
<String Id="OverallProgressPackageText">Initializing...</String>
|
||||
<String Id="ProgressCancelButton">&Cancel</String>
|
||||
<String Id="ModifyHeader">Modify Setup</String>
|
||||
<String Id="ModifyRepairButton">&Repair</String>
|
||||
<String Id="ModifyUninstallButton">&Uninstall</String>
|
||||
<String Id="ModifyCloseButton">&Close</String>
|
||||
<String Id="SuccessRepairHeader">Repair Successfully Completed</String>
|
||||
<String Id="SuccessUninstallHeader">Uninstall Successfully Completed</String>
|
||||
<String Id="SuccessInstallHeader">Installation Successfully Completed</String>
|
||||
<String Id="SuccessHeader">Setup Successful</String>
|
||||
<String Id="SuccessLaunchButton">&Launch Lumberyard Setup Assistant</String>
|
||||
<String Id="SuccessRestartText">You must restart your computer before you can use the software.</String>
|
||||
<String Id="SuccessRestartButton">&Restart</String>
|
||||
<String Id="FailureHeader">Setup Failed</String>
|
||||
<String Id="FailureInstallHeader">Setup Failed</String>
|
||||
<String Id="FailureUninstallHeader">Uninstall Failed</String>
|
||||
<String Id="FailureRepairHeader">Repair Failed</String>
|
||||
<String Id="FailureHyperlinkLogText">One or more issues caused the setup to fail. Please fix the issues and then retry setup. For more information see the <a href="#">log file</a>.</String>
|
||||
<String Id="FailureRestartText">You must restart your computer to complete the rollback of the software.</String>
|
||||
<String Id="FailureRestartButton">&Restart</String>
|
||||
<String Id="FailureCloseButton">&Close</String>
|
||||
<String Id="FilesInUseHeader">Files In Use</String>
|
||||
<String Id="FilesInUseLabel">The following applications are using files that need to be updated:</String>
|
||||
<String Id="FilesInUseCloseRadioButton">Close the &applications and attempt to restart them.</String>
|
||||
<String Id="FilesInUseDontCloseRadioButton">&Do not close applications. A reboot will be required.</String>
|
||||
<String Id="FilesInUseOkButton">&OK</String>
|
||||
<String Id="FilesInUseCancelButton">&Cancel</String>
|
||||
</WixLocalization>
|
||||
@@ -1,85 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Theme xmlns="http://wixtoolset.org/schemas/thmutil/2010">
|
||||
<Window Width="485" Height="300" HexStyle="100a0000" FontId="0">#(loc.Caption)</Window>
|
||||
<Font Id="0" Height="-12" Weight="500" Foreground="000000" Background="FFFFFF">Segoe UI</Font>
|
||||
<Font Id="1" Height="-24" Weight="500" Foreground="000000">Segoe UI</Font>
|
||||
<Font Id="2" Height="-22" Weight="500" Foreground="666666">Segoe UI</Font>
|
||||
<Font Id="3" Height="-12" Weight="500" Foreground="000000" Background="FFFFFF">Segoe UI</Font>
|
||||
<Font Id="4" Height="-12" Weight="500" Foreground="ff0000" Background="FFFFFF" Underline="yes">Segoe UI</Font>
|
||||
|
||||
<Image X="11" Y="11" Width="64" Height="64" ImageFile="logo.png" Visible="yes"/>
|
||||
<Text X="80" Y="11" Width="-11" Height="64" FontId="1" Visible="yes" DisablePrefix="yes">#(loc.Title)</Text>
|
||||
|
||||
<Page Name="Help">
|
||||
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.HelpHeader)</Text>
|
||||
<Text X="11" Y="112" Width="-11" Height="-35" FontId="3" DisablePrefix="yes">#(loc.HelpText)</Text>
|
||||
<Button Name="HelpCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.HelpCloseButton)</Button>
|
||||
</Page>
|
||||
<Page Name="Install">
|
||||
<Text X="11" Y="80" Width="-11" Height="32" FontId="2" DisablePrefix="yes">#(loc.InstallHeader)</Text>
|
||||
<Hypertext X="11" Y="121" Width="-11" Height="64" FontId="3">#(loc.InstallMessage)</Hypertext>
|
||||
<Hypertext X="11" Y="161" Width="-11" Height="64" FontId="3">#(loc.InstallEulaAgreementText)</Hypertext>
|
||||
<Text Name="InstallVersion" X="185" Y="-81" Width="-11" Height="17" FontId="3" DisablePrefix="yes" HideWhenDisabled="yes">#(loc.InstallVersion)</Text>
|
||||
<Button Name="OptionsButton" X="-171" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.InstallOptionsButton)</Button>
|
||||
<Button Name="InstallButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.InstallInstallButton)</Button>
|
||||
<Button Name="WelcomeCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.InstallCloseButton)</Button>
|
||||
</Page>
|
||||
<Page Name="Options">
|
||||
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.OptionsHeader)</Text>
|
||||
<Text X="11" Y="121" Width="-11" Height="17" FontId="3" DisablePrefix="yes">#(loc.OptionsLocationLabel)</Text>
|
||||
<Editbox Name="FolderEditbox" X="11" Y="143" Width="-91" Height="21" TabStop="yes" FontId="3" FileSystemAutoComplete="yes" />
|
||||
<Button Name="BrowseButton" X="-11" Y="142" Width="75" Height="23" TabStop="yes" FontId="3">#(loc.OptionsBrowseButton)</Button>
|
||||
<Button Name="OptionsOkButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.OptionsOkButton)</Button>
|
||||
<Button Name="OptionsCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.OptionsCancelButton)</Button>
|
||||
</Page>
|
||||
<Page Name="FilesInUse">
|
||||
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.FilesInUseHeader)</Text>
|
||||
<Text X="11" Y="121" Width="-11" Height="34" FontId="3" DisablePrefix="yes">#(loc.FilesInUseLabel)</Text>
|
||||
<Text Name="FilesInUseText" X="11" Y="150" Width="-11" Height="-86" FontId="3" DisablePrefix="yes" HexStyle="0x0000C000"></Text>
|
||||
|
||||
<Button Name="FilesInUseCloseRadioButton" X="11" Y="-60" Width="-11" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes" HexStyle="0x000009">#(loc.FilesInUseCloseRadioButton)</Button>
|
||||
<Button Name="FilesInUseDontCloseRadioButton" X="11" Y="-40" Width="-11" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes" HexStyle="0x000009">#(loc.FilesInUseDontCloseRadioButton)</Button>
|
||||
|
||||
<Button Name="FilesInUseOkButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.FilesInUseOkButton)</Button>
|
||||
<Button Name="FilesInUseCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.FilesInUseCancelButton)</Button>
|
||||
</Page>
|
||||
<Page Name="Progress">
|
||||
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.ProgressHeader)</Text>
|
||||
|
||||
<Text X="11" Y="121" Width="75" Height="17" FontId="3" DisablePrefix="yes" >#(loc.DownloadLabel)</Text>
|
||||
<Text Name="CacheProgressPackageText" X="100" Y="121" Width="-5" Height="17" FontId="3" DisablePrefix="yes">#(loc.OverallProgressPackageText)</Text>
|
||||
<Progressbar Name="CacheProgressbar" X="11" Y="143" Width="-11" Height="20" />
|
||||
|
||||
<Text X="11" Y="181" Width="75" Height="17" FontId="3" DisablePrefix="yes">#(loc.ProgressLabel)</Text>
|
||||
<Text Name="ExecuteProgressPackageText" X="100" Y="181" Width="-5" Height="17" FontId="3" DisablePrefix="yes"></Text>
|
||||
<Progressbar Name="ExecuteProgressbar" X="11" Y="203" Width="-11" Height="20" />
|
||||
|
||||
<Button Name="ProgressCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ProgressCancelButton)</Button>
|
||||
</Page>
|
||||
<Page Name="Modify">
|
||||
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.ModifyHeader)</Text>
|
||||
<Button Name="RepairButton" X="-171" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.ModifyRepairButton)</Button>
|
||||
<Button Name="UninstallButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ModifyUninstallButton)</Button>
|
||||
<Button Name="ModifyCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ModifyCloseButton)</Button>
|
||||
</Page>
|
||||
<Page Name="Success">
|
||||
<Text Name="SuccessHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessHeader)</Text>
|
||||
<Text Name="SuccessInstallHeader" X="75" Y="110" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessInstallHeader)</Text>
|
||||
<Text Name="SuccessRepairHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessRepairHeader)</Text>
|
||||
<Text Name="SuccessUninstallHeader" X="75" Y="110" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessUninstallHeader)</Text>
|
||||
<Button Name="LaunchButton" X="-142" Y="-30" Width="200" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.SuccessLaunchButton)</Button>
|
||||
<Text Name="SuccessRestartText" X="-11" Y="-51" Width="400" Height="34" FontId="3" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessRestartText)</Text>
|
||||
<Button Name="SuccessRestartButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.SuccessRestartButton)</Button>
|
||||
</Page>
|
||||
<Page Name="Failure">
|
||||
<Text Name="FailureHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureHeader)</Text>
|
||||
<Text Name="FailureInstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureInstallHeader)</Text>
|
||||
<Text Name="FailureUninstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureUninstallHeader)</Text>
|
||||
<Text Name="FailureRepairHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureRepairHeader)</Text>
|
||||
<Hypertext Name="FailureLogFileLink" X="11" Y="121" Width="-11" Height="42" FontId="3" TabStop="yes" HideWhenDisabled="yes">#(loc.FailureHyperlinkLogText)</Hypertext>
|
||||
<Hypertext Name="FailureMessageText" X="22" Y="163" Width="-11" Height="51" FontId="3" TabStop="yes" HideWhenDisabled="yes" />
|
||||
<Text Name="FailureRestartText" X="-11" Y="-51" Width="400" Height="34" FontId="3" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureRestartText)</Text>
|
||||
<Button Name="FailureRestartButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.FailureRestartButton)</Button>
|
||||
<Button Name="FailureCloseButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.FailureCloseButton)</Button>
|
||||
</Page>
|
||||
</Theme>
|
||||
@@ -1,29 +0,0 @@
|
||||
#
|
||||
# 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 os
|
||||
import SignTool
|
||||
|
||||
|
||||
def SignLumberyardExes(unpackedLocation,
|
||||
signingType,
|
||||
timestampServer,
|
||||
verbose,
|
||||
filesToSign):
|
||||
for file in filesToSign:
|
||||
fileFullPath = os.path.join(unpackedLocation, file)
|
||||
|
||||
SignTool.signtoolSignAndVerifyFile(fileFullPath,
|
||||
os.path.dirname(fileFullPath),
|
||||
os.path.dirname(fileFullPath),
|
||||
signingType,
|
||||
timestampServer,
|
||||
verbose)
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0782cab23b9ad88124296586afb2ed8e88f2ab4020786efe05da396d8f33e8ec
|
||||
size 1542
|
||||
@@ -1,212 +0,0 @@
|
||||
#
|
||||
# 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 shutil
|
||||
import subprocess
|
||||
|
||||
from BuildInstallerUtils import *
|
||||
|
||||
# Signing cmd example
|
||||
# signtool.exe sign /f c:\codesigning\lumberyard.pfx /tr http://tsa.starfieldtech.com /td SHA256 /p thePasswordSeeNotesBelow %f
|
||||
|
||||
# Verify cmd example.
|
||||
# signtool.exe verify /v /pa %f
|
||||
|
||||
# SignTool COMMANDLINE TEMPLATES
|
||||
signtoolCommandBase = "signtool.exe {commandType} {verbose} {commandTypeParams} {filename}"
|
||||
signtoolKeySignParams = "/fd SHA256 /f {certificatePath} /p {password}"
|
||||
signtoolNameSignParams = '/fd SHA256 /n "{certificateName}"'
|
||||
signtoolTimestampParams = "/tr {timestampServerURL} /td SHA256"
|
||||
signtoolVerifyParams = "/pa /tw /hash SHA256"
|
||||
|
||||
|
||||
class SignType:
|
||||
def __init__(self, signParamsString):
|
||||
self.signtoolParamsTemplate = signParamsString
|
||||
|
||||
def getSigntoolParamsTemplate(self):
|
||||
return self.signtoolParamsTemplate
|
||||
|
||||
def getSigntoolParams(self):
|
||||
raise NotImplementedError(self.getSigntoolParams.__name__)
|
||||
|
||||
|
||||
class KeySigning(SignType):
|
||||
def __init__(self, key, password):
|
||||
SignType.__init__(self, signtoolKeySignParams)
|
||||
self.privateKey = key
|
||||
self.password = password
|
||||
|
||||
def getSigntoolParams(self):
|
||||
return self.getSigntoolParamsTemplate().format(certificatePath=self.privateKey, password=self.password)
|
||||
|
||||
|
||||
class NameSigning(SignType):
|
||||
def __init__(self, name):
|
||||
SignType.__init__(self, signtoolNameSignParams)
|
||||
self.certName = name
|
||||
|
||||
def getSigntoolParams(self):
|
||||
return self.getSigntoolParamsTemplate().format(certificateName=self.certName)
|
||||
|
||||
|
||||
def getSignToolVerboseCommand(verboseMode):
|
||||
if verboseMode:
|
||||
return " /v"
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def buildSignCommand(filename, signingType, verbose):
|
||||
verboseCmd = getSignToolVerboseCommand(verbose)
|
||||
|
||||
signtoolParams = signingType.getSigntoolParams()
|
||||
signtoolCommand = signtoolCommandBase.format(commandType="sign",
|
||||
verbose=verboseCmd, commandTypeParams=signtoolParams, filename = filename)
|
||||
return signtoolCommand
|
||||
|
||||
|
||||
def signtoolTestCredentials(signingType, timestampServer, verbose):
|
||||
testFileName = "testFile"
|
||||
if os.path.exists(testFileName):
|
||||
os.remove(testFileName)
|
||||
|
||||
with open(testFileName, "wb") as testSign:
|
||||
testSign.seek(1023)
|
||||
testSign.write("0")
|
||||
|
||||
# error codes for sign tool are only 0 or 1. No way to get a success without
|
||||
# a valid .exe to sign, so the only way to test is to try to sign a file
|
||||
# that will be considered an unrecognized format, and parse the output to
|
||||
# see if the error was with the password.
|
||||
signtoolCommand = buildSignCommand(testFileName, signingType, verbose)
|
||||
sp = subprocess.Popen(signtoolCommand, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
output, error = sp.communicate()
|
||||
|
||||
validPassword = False
|
||||
if error:
|
||||
# if find returns -1, it means it couldnt find password incorrect text,
|
||||
# meaning it is a valid password.
|
||||
validPassword = error.splitlines()[0].lower().find("password is not correct") == -1
|
||||
|
||||
os.remove(testFileName)
|
||||
return validPassword
|
||||
|
||||
|
||||
def signtoolSignFile(filename, signingType, verbose):
|
||||
signtoolCommand = buildSignCommand(filename, signingType, verbose)
|
||||
|
||||
verbose_print(verbose, '\n{}\n'.format(signtoolCommand))
|
||||
return os.system(signtoolCommand) == 0
|
||||
|
||||
|
||||
def signtoolTimestamp(filename, timestampServer, verbose):
|
||||
verboseCmd = getSignToolVerboseCommand(verbose)
|
||||
|
||||
signtoolParams = signtoolTimestampParams.format(timestampServerURL=timestampServer)
|
||||
signtoolCommand = signtoolCommandBase.format(commandType="timestamp",
|
||||
verbose=verboseCmd,
|
||||
commandTypeParams=signtoolParams,
|
||||
filename=filename)
|
||||
|
||||
verbose_print(verbose, '\n{}\n'.format(signtoolCommand))
|
||||
|
||||
success = False
|
||||
while not success:
|
||||
success = (os.system(signtoolCommand) == 0)
|
||||
|
||||
return success
|
||||
|
||||
|
||||
def signtoolVerifySign(filename, verbose):
|
||||
verboseCmd = getSignToolVerboseCommand(verbose)
|
||||
|
||||
signtoolCommand = signtoolCommandBase.format(commandType="verify",
|
||||
verbose=verboseCmd,
|
||||
commandTypeParams=signtoolVerifyParams,
|
||||
filename=filename)
|
||||
|
||||
verbose_print(verbose, '\n{}\n'.format(signtoolCommand))
|
||||
return_result = os.system(signtoolCommand) == 0
|
||||
return return_result
|
||||
|
||||
|
||||
def signtoolSignAndVerifyFile(filename,
|
||||
workingDir,
|
||||
sourceDir,
|
||||
signingType,
|
||||
timestampServer,
|
||||
verbose):
|
||||
shouldCopy = workingDir is not sourceDir
|
||||
filePath = os.path.join(workingDir, filename)
|
||||
|
||||
# most of the time that signing fails, it is due to the signing server not responding.
|
||||
# keep retrying until the signing is successful.
|
||||
# NOTE: might want to change this to a for loop to limit number of retries per file.
|
||||
success = False
|
||||
attemptsMade = 0
|
||||
|
||||
while not success:
|
||||
# SIGN THE FILE
|
||||
success = signtoolSignFile(filePath, signingType, verbose)
|
||||
assert success, 'Failed to sign file {}. Most likely the password entered is incorrect.'.format(filename)
|
||||
|
||||
# TIMESTAMP THE FILE
|
||||
success = signtoolTimestamp(filePath, timestampServer, verbose)
|
||||
assert success, "Failed to contact the timestamp server."
|
||||
|
||||
# VERIFY SUCCESSFUL SIGNING
|
||||
success = signtoolVerifySign(filePath, verbose)
|
||||
attemptsMade += 1
|
||||
|
||||
if not success:
|
||||
verbose_print(verbose, 'Failed to sign file {}'.format(filename))
|
||||
if shouldCopy:
|
||||
# delete the original file, copy back from source, and re-sign
|
||||
os.remove(filePath)
|
||||
shutil.copy(os.path.join(sourceDir, filename), filePath)
|
||||
|
||||
verbose_print(verbose, 'Attempts made to sign file {}: {}\n'.format(filename, attemptsMade))
|
||||
|
||||
return success
|
||||
|
||||
|
||||
def signtoolSignAndVerifyFiles(fileList,
|
||||
workingDir,
|
||||
sourceDir,
|
||||
signingType,
|
||||
timestampServer,
|
||||
verbose):
|
||||
|
||||
for filename in fileList:
|
||||
signtoolSignAndVerifyFile(filename,
|
||||
workingDir,
|
||||
sourceDir,
|
||||
signingType,
|
||||
timestampServer,
|
||||
verbose)
|
||||
|
||||
|
||||
def signtoolSignAndVerifyType(fileExtension,
|
||||
workingDir,
|
||||
sourceDir,
|
||||
signingType,
|
||||
timestampServer,
|
||||
verbose):
|
||||
# Sign each file in a directory that has the given file extension
|
||||
for file in os.listdir(workingDir):
|
||||
if file.endswith(fileExtension):
|
||||
signtoolSignAndVerifyFile(os.path.basename(file),
|
||||
workingDir,
|
||||
sourceDir,
|
||||
signingType,
|
||||
timestampServer,
|
||||
verbose)
|
||||
@@ -1,186 +0,0 @@
|
||||
#
|
||||
# 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 ctypes
|
||||
import filecmp
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
parser = argparse.ArgumentParser(description='Tests the Lumberyard Installer bootstrapper.')
|
||||
parser.add_argument('-v', '--verbose', action='store_true', help='Enables logging messages (default False)')
|
||||
parser.add_argument('-k', '--keep', action='store_true', help="Don't delete temp files")
|
||||
parser.add_argument('--packageRoot', default=None, help="Source content to test against, required for use. (default None)")
|
||||
parser.add_argument('--lyVersion', default="0.0.0.0", help="Version to use when generated artifacts (default '0.0.0.0')")
|
||||
parser.add_argument('--target', default='%temp%\\installertest\\TestInstall', help='The location to install Lumberyard. Make sure to use backslashes in the path. (default "%%temp%%\\installertest\\TestInstall"')
|
||||
parser.add_argument('--genRoot', default='%temp%/installertest/', help='Path for temp data (default "%%temp%%/installertest/"')
|
||||
parser.add_argument('--hostURL', default="https://s3-us-west-2.amazonaws.com/lumberyard-streaming-install-test/releases/JoeInstallTest/", help='The URL for the installer to download its packages from (msi + cab files). (default https://s3-us-west-2.amazonaws.com/lumberyard-streaming-install-test/releases/JoeInstallTest/)')
|
||||
parser.add_argument('--testName', default=None, help='The name of the test to run (case insensitive). Will cause an error if no test with that name exists. Will run all tests if not specified. (default None)')
|
||||
bootstrapperName = "automatedTestBootstrapper.exe"
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Installation requires administration privileges.
|
||||
try:
|
||||
is_admin = os.getuid() == 0
|
||||
except AttributeError:
|
||||
is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0
|
||||
|
||||
if not is_admin:
|
||||
sys.exit("Administrator privileges must be enabled to install Lumberyard")
|
||||
|
||||
# Search target for forward slash (/) and replace with back slashes (\)
|
||||
# because the install command doesn't like forward slashes.
|
||||
installTarget = args.target.replace('/', '\\')
|
||||
|
||||
pfxName = "LumberyardDev.pfx"
|
||||
signingArgs = "--privateKey {} --password test".format(pfxName)
|
||||
|
||||
|
||||
# Creates an installer with the given additional arguments. Will leave temp files
|
||||
# even if the keep argument for this script is specified, in order to correctly
|
||||
# perform some tests
|
||||
def makeInstaller(additionalCommands=None):
|
||||
buildInstallerCommand = "BuildInstaller.py --packageRoot {} " \
|
||||
"--lyVersion {} " \
|
||||
"--genRoot {} " \
|
||||
"--bootstrapName {} " \
|
||||
"--hostURL {}".format(args.packageRoot,
|
||||
args.lyVersion,
|
||||
args.genRoot,
|
||||
bootstrapperName,
|
||||
args.hostURL)
|
||||
if args.verbose:
|
||||
buildInstallerCommand += " -v"
|
||||
|
||||
if additionalCommands:
|
||||
buildInstallerCommand += " " + additionalCommands
|
||||
|
||||
os.system(buildInstallerCommand)
|
||||
|
||||
|
||||
# Will create an installer with the given additional arguments, run the installer,
|
||||
# and verify contents of the installed lumberyard with the source package.
|
||||
# Some tests require artifacts of previous installs to exist. If they do not,
|
||||
# a set of artifacts will be created.
|
||||
def createAndTestInstaller(installerSubfolder,
|
||||
additionalCommands,
|
||||
requiresSigning=False,
|
||||
requiresArtifacts=False):
|
||||
if requiresArtifacts:
|
||||
# Create a set of artifacts if none are available
|
||||
if not os.path.exists(os.path.join(args.genRoot, installerSubfolder)):
|
||||
if requiresSigning:
|
||||
makeInstaller(signingArgs)
|
||||
else:
|
||||
makeInstaller()
|
||||
|
||||
makeInstaller(additionalCommands)
|
||||
bootstrapperPath = os.path.join(args.genRoot, installerSubfolder, bootstrapperName)
|
||||
installCommand = "{} /silent InstallFolder={}".format(bootstrapperPath, installTarget)
|
||||
if args.verbose:
|
||||
print("Running installation command:")
|
||||
print("\t{}".format(installCommand))
|
||||
os.system(installCommand)
|
||||
|
||||
# Validate install was successful
|
||||
pathToInstalledPackage = os.path.join(installTarget, args.lyVersion)
|
||||
# filecmp.dircmp failes if you pass in a directory with "%temp%", so %temp% has to be replaced by
|
||||
# temp dir before calling it.
|
||||
scrubbedPackagedRoot = args.packageRoot.replace("%temp%", tempfile.gettempdir())
|
||||
pathToInstalledPackage = pathToInstalledPackage.replace("%temp%", tempfile.gettempdir())
|
||||
installVerifier = filecmp.dircmp(scrubbedPackagedRoot, pathToInstalledPackage)
|
||||
if args.verbose:
|
||||
print("Dif between install and source:")
|
||||
installVerifier.report_full_closure()
|
||||
if installVerifier.left_only or installVerifier.right_only:
|
||||
print("Error: Install failed, source and destination do not match")
|
||||
|
||||
# Uninstall Lumberyard
|
||||
uninstallCommand = "start /WAIT {} /silent /uninstall".format(bootstrapperPath)
|
||||
if args.verbose:
|
||||
print("Running uninstallation command:")
|
||||
print("\t{}".format(uninstallCommand))
|
||||
os.system(uninstallCommand)
|
||||
|
||||
# Validate uninstall was successful
|
||||
if os.path.isdir(args.target):
|
||||
print("Error: Uninstall failed.")
|
||||
|
||||
|
||||
tests = {}
|
||||
|
||||
|
||||
def makeTest(testName,
|
||||
installerSubfolder,
|
||||
additionalCommands,
|
||||
requiresSigning=False,
|
||||
requiresArtifacts=False):
|
||||
|
||||
def _makeTestArgs(testName, *testArgs):
|
||||
tests[testName] = testArgs
|
||||
|
||||
_makeTestArgs(testName, installerSubfolder, additionalCommands, requiresSigning, requiresArtifacts)
|
||||
|
||||
|
||||
def listTests():
|
||||
print('Tests with the following names have been defined:')
|
||||
for testName in tests.keys():
|
||||
print(testName)
|
||||
|
||||
|
||||
def runTest(testName):
|
||||
if args.verbose:
|
||||
print('Performing test "{}"'.format(testName))
|
||||
testArgs = tests[testName]
|
||||
if testArgs is not None and len(testArgs) > 0:
|
||||
createAndTestInstaller(*testArgs)
|
||||
|
||||
|
||||
def runAllTests():
|
||||
for testName in tests.keys():
|
||||
runTest(testName)
|
||||
|
||||
|
||||
# DEFINE TEST CASES
|
||||
|
||||
# Test success cases
|
||||
# Unsigned
|
||||
makeTest("unsigned installer", "unsignedInstaller", None)
|
||||
makeTest("unsigned bootstrap only", "unsignedInstaller", "--bootstrapOnly", False, True)
|
||||
# Signed
|
||||
makeTest("signed installer", "installer", signingArgs, True)
|
||||
makeTest("signed sign only", "installer", signingArgs + " --signOnly", True, True)
|
||||
makeTest("signed bootstrap only", "installer", signingArgs + " --bootstrapOnly", True, True)
|
||||
|
||||
# END DEFINE TEST CASES
|
||||
|
||||
|
||||
# RUN TEST(S)
|
||||
if args.testName is not None:
|
||||
if args.testName.lower() in tests:
|
||||
runTest(args.testName.lower())
|
||||
else:
|
||||
print('Test with the name "{}" does not exist.'.format(args.testName))
|
||||
listTests()
|
||||
else:
|
||||
runAllTests()
|
||||
# END RUN TEST(S)
|
||||
|
||||
|
||||
# CLEAN-UP
|
||||
if not args.keep:
|
||||
import shutil
|
||||
if os.path.exists(args.genRoot):
|
||||
shutil.rmtree(args.genRoot)
|
||||
# END CLEAN-UP
|
||||
@@ -1,10 +0,0 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"Whitelist": [
|
||||
"3rdParty/OpenEXR/2.0/src/openexr-2.0.0/vc/vc7/OpenEXR/debug",
|
||||
"3rdParty/OpenEXR/2.0/src/openexr-2.0.0/vc/vc7/OpenEXR/exrenvmap/Debug",
|
||||
"3rdParty/OpenEXR/2.0/src/openexr-2.0.0/vc/vc7/OpenEXR/exrenvmap/Release",
|
||||
"3rdParty/OpenEXR/2.0/src/openexr-2.0.0/vc/vc7/OpenEXR/exrheader/Debug",
|
||||
"3rdParty/OpenEXR/2.0/src/openexr-2.0.0/vc/vc7/OpenEXR/exrheader/Release",
|
||||
"3rdParty/OpenEXR/2.0/src/openexr-2.0.0/vc/vc7/OpenEXR/exrmakepreview/Debug",
|
||||
"3rdParty/OpenEXR/2.0/src/openexr-2.0.0/vc/vc7/OpenEXR/exrmakepreview/Release",
|
||||
"3rdParty/OpenEXR/2.0/src/openexr-2.0.0/vc/vc7/OpenEXR/exrmaketiled/Debug",
|
||||
"3rdParty/OpenEXR/2.0/src/openexr-2.0.0/vc/vc7/OpenEXR/exrstdattr/Debug",
|
||||
"3rdParty/OpenEXR/2.0/src/openexr-2.0.0/vc/vc7/OpenEXR/IlmImf/Debug",
|
||||
"3rdParty/OpenEXR/2.0/src/openexr-2.0.0/vc/vc7/OpenEXR/IlmImf/Release",
|
||||
"3rdParty/OpenEXR/2.0/src/openexr-2.0.0/vc/vc7/OpenEXR/IlmImfExamples/Debug",
|
||||
"3rdParty/OpenEXR/2.0/src/openexr-2.0.0/vc/vc7/OpenEXR/IlmImfExamples/Release",
|
||||
"3rdParty/OpenEXR/2.0/src/openexr-2.0.0/vc/vc7/OpenEXR/IlmImfTest/Debug",
|
||||
"3rdParty/OpenEXR/2.0/src/openexr-2.0.0/vc/vc7/OpenEXR/IlmImfTest/Release",
|
||||
"3rdParty/OpenEXR/2.0/src/openexr-2.0.0/vc/vc7/OpenEXR/release",
|
||||
"dev/Bin64/EditorPlugins",
|
||||
"dev/SamplesProject/Levels/Samples/Gems_Samples",
|
||||
"dev/Cache/StarterGame/pc/user/log"
|
||||
]
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"_comment": "Folders listed here will only include the content listed. All other content within the folder will be omitted from the installer.",
|
||||
"dev/Bin64vc141": [
|
||||
"EditorPlugins/ProceduralMaterialEditorPlugin.dll",
|
||||
"EditorPlugins/ProceduralMaterialEditorPlugin.exp",
|
||||
"EditorPlugins/ProceduralMaterialEditorPlugin.lib",
|
||||
"EditorPlugins/ProceduralMaterialEditorPlugin.dll.manifest",
|
||||
"rc/d3dcompiler_47.dll",
|
||||
"rc/d3dcsx_47.dll",
|
||||
"rc/d3dx11_43.dll",
|
||||
"rc/dbghelp.dll",
|
||||
"rc/opengl32sw.dll",
|
||||
"rc/PVRTexLib_License.txt",
|
||||
"rc/xinput1_3.dll",
|
||||
"D3DCompiler_43.dll",
|
||||
"d3dcompiler_46.dll",
|
||||
"d3dcompiler_47.dll",
|
||||
"d3dcsx_46.dll",
|
||||
"d3dcsx_47.dll",
|
||||
"d3dx11_43.dll",
|
||||
"dbghelp.dll",
|
||||
"glut32.dll",
|
||||
"imguilib.dll",
|
||||
"imguilib.dll.manifest",
|
||||
"imguilib.exp",
|
||||
"imguilib.lib",
|
||||
"libeay32.dll",
|
||||
"LuaCompiler.exe",
|
||||
"Microsoft.VC90.CRT.manifest",
|
||||
"msvcr90.dll",
|
||||
"nvToolsExt64_1.dll",
|
||||
"PhysX3CharacterKinematicPROFILE_x64.dll",
|
||||
"PhysX3CommonPROFILE_x64.dll",
|
||||
"PhysX3CookingPROFILE_x64.dll",
|
||||
"PhysX3GpuPROFILE_x64.dll",
|
||||
"PhysX3PROFILE_x64.dll",
|
||||
"PhysXDevice64.dll",
|
||||
"PVRTexLib_License.txt",
|
||||
"PxFoundationPROFILE_x64.dll",
|
||||
"PxPvdSDKPROFILE_x64.dll",
|
||||
"SDL2.dll",
|
||||
"ssleay32.dll",
|
||||
"substance_d3d11pc_blend.dll",
|
||||
"substance_linker.dll",
|
||||
"substance_sse2_blend.dll",
|
||||
"ToolsCrashUploader.exe",
|
||||
"ToolsCrashUploader.exe.manifest",
|
||||
"xinput1_3.dll"
|
||||
]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2f464626a1ce6148798c6c378865a66fceefe0e8541c904730485e38d21203b6
|
||||
size 43973
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user