#! /usr/bin/env python # # Copyright (C) 2017 Sony Interactive Entertainment Inc. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import argparse import json import os import sys try: from urllib.request import urlopen, Request from urllib.error import URLError except ImportError: from urllib2 import urlopen, Request, URLError PUBLIC_GITHUB_API_ENDPOINT = 'https://api.github.com/' DESCRIPTION = '''Downloads a release binary from a GitHub repository. (Requests the latest release unless a specific tag is provided.) Intended for download of vswhere.exe and WinCairoRequirements.zip, but may be used for arbitrary binaries / repositories. Checks whether the desired version already exists in the output directory (by looking for a .version file saved alongside the release binary) -- if so, download is skipped; otherwise any existing version is overwritten. ''' class Status: DOWNLOADED = 0 UP_TO_DATE = 1 COULD_NOT_FIND = 2 USING_EXISTING = 3 def parse_args(argv): parser = argparse.ArgumentParser(description=DESCRIPTION, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('repo', help='GitHub repository slug (e.g., "org/repo")') parser.add_argument('filename', help='filename of release binary to download (e.g., "foo.exe", "bar.zip")') parser.add_argument('-o', '--output-dir', default='.', help='output directory (defaults to working directory)') parser.add_argument('-e', '--endpoint', default=PUBLIC_GITHUB_API_ENDPOINT, help='GitHub API endpoint (defaults to api.github.com)') parser.add_argument('-t', '--token', default=None, help='GitHub API OAuth token (for private repos/endpoints)') parser.add_argument('-r', '--release-tag', default=None, help='release tag to download (defaults to latest)') return parser.parse_args(argv) def find_release(endpoint, repo, filename, token, tag): release_name = 'tags/{}'.format(tag) if tag else 'latest' url = '{}/repos/{}/releases/{}'.format(endpoint.rstrip('/'), repo, release_name) request = Request(url) request.add_header('Accept', 'application/vnd.github.v3+json') if token: request.add_header('Authorization', 'token {}'.format(token)) try: response = urlopen(request) except URLError as error: print(error) return None, None data = json.loads(response.read()) for asset in data['assets']: if asset['name'] == filename: version_info = {'tag_name': data['tag_name'], 'updated_at': asset['updated_at']} return asset['url'], version_info return None, None def download_release(source_url, target_path, token): request = Request(source_url) request.add_header('Accept', 'application/octet-stream') if token: request.add_header('Authorization', 'token {}'.format(token)) with open(target_path, 'wb') as file: file.write(urlopen(request).read()) def load_version_info(version_info_path): if not os.path.exists(version_info_path): return None with open(version_info_path) as file: return json.load(file) def save_version_info(version_info_path, version_info): with open(version_info_path, 'w') as file: json.dump(version_info, file) def main(argv): args = parse_args(argv) binary_path = os.path.join(args.output_dir, args.filename) version_info_path = binary_path + '.version' print('Updating {}...'.format(args.filename)) existing_version_info = load_version_info(version_info_path) if existing_version_info: print('Found existing release: {}'.format(existing_version_info['tag_name'])) else: print('No existing release found.') release_title = 'release "{}"'.format(args.release_tag) if args.release_tag else 'latest release' print('Seeking {} from {}...'.format(release_title, args.repo)) release_url, target_version_info = find_release(args.endpoint, args.repo, args.filename, args.token, args.release_tag) if not target_version_info: if existing_version_info: print('Falling back to existing release!') return Status.USING_EXISTING print('No release found!') return Status.COULD_NOT_FIND print('Found release to download: {}'.format(target_version_info['tag_name'])) if target_version_info == existing_version_info: print('Already up-to-date!') return Status.UP_TO_DATE if not os.path.exists(args.output_dir): os.makedirs(args.output_dir) print('Downloading to {}...'.format(os.path.abspath(args.output_dir))) download_release(release_url, binary_path, args.token) save_version_info(version_info_path, target_version_info) print('Done!') return Status.DOWNLOADED if __name__ == '__main__': result = main(sys.argv[1:]) if result == Status.COULD_NOT_FIND: sys.exit(1)