Files
UnrealEngine/Engine/Source/ThirdParty/OpenVDB/openvdb-12.0.0/ci/download_houdini.py
2025-05-18 13:04:45 +08:00

221 lines
7.4 KiB
Python

#!/usr/bin/env python3
#
# Copyright Contributors to the OpenVDB Project
# SPDX-License-Identifier: Apache-2.0
#
# Python script to download the latest Houdini builds
# using the SideFX download API:
#
# https://www.sidefx.com/docs/api/download/index.html
#
# Authors: Dan Bailey, SideFX
import time
import sys
import re
import shutil
import json
import base64
import requests
import hashlib
import os
import argparse
import copy
# For progress bar printing
try:
from tqdm import tqdm
has_tqdm = True
except:
has_tqdm = False
pass
# Code that provides convenient Python wrappers to call into the API:
def service(
access_token_url, client_id, client_secret_key, endpoint_url,
access_token=None, access_token_expiry_time=None):
if (access_token is None or
access_token_expiry_time is None or
access_token_expiry_time < time.time()):
access_token, access_token_expiry_time = (
get_access_token_and_expiry_time(
access_token_url, client_id, client_secret_key))
return _Service(
endpoint_url, access_token, access_token_expiry_time)
class _Service(object):
def __init__(
self, endpoint_url, access_token, access_token_expiry_time):
self.endpoint_url = endpoint_url
self.access_token = access_token
self.access_token_expiry_time = access_token_expiry_time
def __getattr__(self, attr_name):
return _APIFunction(attr_name, self)
class _APIFunction(object):
def __init__(self, function_name, service):
self.function_name = function_name
self.service = service
def __getattr__(self, attr_name):
# This isn't actually an API function, but a family of them. Append
# the requested function name to our name.
return _APIFunction(
"{0}.{1}".format(self.function_name, attr_name), self.service)
def __call__(self, *args, **kwargs):
return call_api_with_access_token(
self.service.endpoint_url, self.service.access_token,
self.function_name, args, kwargs)
#---------------------------------------------------------------------------
# Code that implements authentication and raw calls into the API:
def get_access_token_and_expiry_time(
access_token_url, client_id, client_secret_key):
"""Given an API client (id and secret key) that is allowed to make API
calls, return an access token that can be used to make calls.
"""
response = requests.post(
access_token_url,
headers={
"Authorization": u"Basic {0}".format(
base64.b64encode(
"{0}:{1}".format(
client_id, client_secret_key
).encode()
).decode('utf-8')
),
})
if response.status_code != 200:
raise AuthorizationError(response.status_code, response.text)
response_json = response.json()
access_token_expiry_time = time.time() - 2 + response_json["expires_in"]
return response_json["access_token"], access_token_expiry_time
class AuthorizationError(Exception):
"""Raised from the client if the server generated an error while generating
an access token.
"""
def __init__(self, http_code, message):
super(AuthorizationError, self).__init__(message)
self.http_code = http_code
def call_api_with_access_token(
endpoint_url, access_token, function_name, args, kwargs):
"""Call into the API using an access token that was returned by
get_access_token.
"""
response = requests.post(
endpoint_url,
headers={
"Authorization": "Bearer " + access_token,
},
data=dict(
json=json.dumps([function_name, args, kwargs]),
))
if response.status_code == 200:
return response.json()
raise APIError(response.status_code, response.text)
class APIError(Exception):
"""Raised from the client if the server generated an error while calling
into the API.
"""
def __init__(self, http_code, message):
super(APIError, self).__init__(message)
self.http_code = http_code
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Download a Houdini Installation')
parser.add_argument('version', type=str, help='Major.Minor version of Houdini to download')
parser.add_argument('platform', type=str, help='Platform target')
parser.add_argument('--prod', action='store_true', help='Only download production builds')
parser.add_argument('--list', action='store_true', help='Just list the available builds and exit.')
args = parser.parse_args()
version = args.version
platform = args.platform
only_production = args.prod
user_client_id = os.getenv('HOUDINI_CLIENT_ID')
user_client_secret_key = os.getenv('HOUDINI_SECRET_KEY')
if not re.match('[0-9][0-9]\.[0-9]$', version):
raise IOError('Invalid Houdini Version "%s", expecting in the form "major.minor" such as "16.0"' % version)
service = service(
access_token_url="https://www.sidefx.com/oauth2/application_token",
client_id=user_client_id,
client_secret_key=user_client_secret_key,
endpoint_url="https://www.sidefx.com/api/",
)
releases_list = service.download.get_daily_builds_list(
product='houdini', version=version, platform=platform, only_production=only_production)
print('Available builds:')
for rel in releases_list:
rel = copy.deepcopy(rel)
if 'third_party_libraries' in rel:
# Don't print these
del rel['third_party_libraries']
print(rel)
if args.list:
sys.exit(0)
print('Selecting build: ' + releases_list[0]['build'])
latest_release = service.download.get_daily_build_download(
product='houdini', version=version, platform=platform, build=releases_list[0]['build'])
print(latest_release)
# Can't do this procedurally as latest_release['filename'] can contain
# multiple periods and may have multiple trailing extensions...
extension = ''
if 'linux' in platform: extension = 'tar.gz'
elif 'macos' in platform: extension = 'dmg'
elif 'win64' in platform: extension = 'exe'
assert(extension in latest_release['filename'])
# Download the file and save it as hou.extension
local_filename = 'hou.' + extension
print('Writing to "' + local_filename + '"')
response = requests.get(latest_release['download_url'], stream=True)
if response.status_code == 200:
response.raw.decode_content = True
if has_tqdm:
file_size = int(response.headers.get('Content-Length', 0))
desc = "(Unknown total file size)" if file_size == 0 else ""
with tqdm.wrapattr(response.raw, "read", total=file_size, desc=desc) as r_raw:
with open(local_filename, 'wb') as f:
shutil.copyfileobj(r_raw, f)
else:
with open(local_filename, 'wb') as f:
shutil.copyfileobj(response.raw, f)
else:
raise Exception('Error downloading file!')
# Verify the file checksum is matching
file_hash = hashlib.md5()
with open(local_filename, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
file_hash.update(chunk)
if file_hash.hexdigest() != latest_release['hash']:
raise Exception('Checksum does not match!')