Files
shaka-player/build/shakaBuildHelpers.py
T
Joey Parrish 7e6a0f38ff fix: Correct license headers in misc. files
This corrects/normalizes license headers in misc. files, such as
config files, docs, build tools, tests, and externs.  This does not
affect the compiled output, and is only done for consistency.

Issue #2638

Change-Id: I9d8da2de55243b08d7df2b743aac73c6f15e858a
2020-06-09 16:13:56 -07:00

360 lines
11 KiB
Python

# Copyright 2016 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Contains helper functions used in the build scripts.
This uses two environment variables to help with debugging the scripts:
PRINT_ARGUMENTS - If set, will print any arguments to subprocess.
RAISE_INTERRUPT - Will raise keyboard interrupts rather than swallowing them.
"""
from __future__ import print_function
import errno
import json
import logging
import os
import platform
import re
import subprocess
import sys
import time
def _node_modules_last_update_path():
return os.path.join(get_source_base(), 'node_modules', '.last_update')
def _modules_need_update():
try:
last_update = os.path.getmtime(_node_modules_last_update_path())
if last_update > time.time():
# Update time in the future! Something is wrong, so update.
return True
package_json_path = os.path.join(get_source_base(), 'package.json')
last_json_change = os.path.getmtime(package_json_path)
if last_json_change >= last_update:
# The json file has changed, so update.
return True
except:
# No such file, so we should update.
return True
return False
def _parse_version(version):
"""Converts the given string version to a tuple of numbers."""
# Handle any prerelease or build metadata, such as -beta or -g1234
if '-' in version:
version, trailer = version.split('-')
else:
# Versions without a trailer should sort later than those with a trailer.
# For example, 2.5.0-beta comes before 2.5.0.
# To accomplish this, we synthesize a trailer which sorts later than any
# _reasonable_ alphanumeric version trailer would. These characters have a
# high value in ASCII.
trailer = '}}}'
numeric_parts = [int(i) for i in version.split('.')]
return tuple(numeric_parts + [trailer])
def get_source_base():
"""Returns the absolute path to the source code base."""
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
def is_linux():
"""Determines if the system is Linux."""
return platform.uname()[0] == 'Linux'
def is_darwin():
"""Determines if the system is a Mac."""
return platform.uname()[0] == 'Darwin'
def is_windows():
"""Determines if the system is native Windows (i.e. not Cygwin)."""
return platform.uname()[0] == 'Windows'
def is_cygwin():
"""Determines if the system is Cygwin (i.e. not native Windows)."""
return 'CYGWIN' in platform.uname()[0]
def quote_argument(arg):
"""Wraps the given argument in quotes if needed.
This is so execute_subprocess output can be copied and pasted into a shell.
Args:
arg: The string to convert.
Returns:
The quoted argument.
"""
if '"' in arg:
assert "'" not in arg
return "'" + arg + "'"
if "'" in arg:
assert '"' not in arg
return '"' + arg + '"'
if ' ' in arg:
return '"' + arg + '"'
return arg
def open_file(*args, **kwargs):
"""Opens a file with the given mode and options."""
if sys.version_info[0] == 2:
# Python 2 returns byte strings even in text mode, so the encoding doesn't
# matter.
return open(*args, **kwargs)
else:
# Python 3 requires setting an encoding so it reads strings. The default
# is based on the platform, which on Windows, isn't UTF-8.
return open(encoding='utf8', *args, **kwargs)
def execute_subprocess(args, **kwargs):
"""Executes the given command using subprocess.
If PRINT_ARGUMENTS environment variable is set, this will first print the
arguments.
Args:
args: A list of strings for the subprocess to run.
kwargs: Extra keyword arguments to pass to Popen.
Returns:
The same value as subprocess.Popen.
"""
if os.environ.get('PRINT_ARGUMENTS'):
logging.info(' '.join([quote_argument(x) for x in args]))
try:
return subprocess.Popen(args, **kwargs)
except OSError as e:
if e.errno == errno.ENOENT:
logging.error('*** A required dependency is missing: %s', args[0])
# Exit early to avoid showing a confusing stack trace.
sys.exit(1)
raise
def execute_get_code(args):
"""Calls execute_subprocess and gets return code."""
obj = execute_subprocess(args)
obj.communicate()
return obj.returncode
def execute_get_output(args):
"""Calls execute_subprocess and get the stdout of the process."""
obj = execute_subprocess(args, stdout=subprocess.PIPE)
# This will block until the process terminates, storing the stdout in a string
stdout = obj.communicate()[0]
if obj.returncode != 0:
raise subprocess.CalledProcessError(obj.returncode, args[0], stdout)
return stdout
def cygwin_safe_path(path):
"""Converts the given path to a Cygwin path, if needed."""
if is_cygwin():
return execute_get_output(['cygpath', '-w', path]).strip()
else:
return path
def git_version():
"""Gets the version of the library from git."""
# Check if the shaka-player source base directory has '.git' file.
git_path = os.path.join(get_source_base(), '.git')
if not os.path.exists(git_path):
raise RuntimeError('no .git file is in the shaka-player repository.')
else:
try:
# Check git tags for a version number, noting if the sources are dirty.
cmd_line = ['git', '-C', get_source_base(), 'describe', '--tags', '--dirty']
return execute_get_output(cmd_line).decode('utf8').strip()
except subprocess.CalledProcessError:
raise RuntimeError('Unable to determine library version!')
def npm_version(is_dirty=False):
"""Gets the version of the library from NPM."""
try:
base = cygwin_safe_path(get_source_base())
cmd = 'npm.cmd' if is_windows() else 'npm'
cmd_line = [cmd, '--prefix', base, 'ls', 'shaka-player']
text = execute_get_output(cmd_line).decode('utf8')
except subprocess.CalledProcessError as e:
text = e.output.decode('utf8')
match = re.search(r'shaka-player@(.*) ', text)
if match:
return match.group(1) + ('-npm-dirty' if is_dirty else '')
raise RuntimeError('Unable to determine library version!')
def calculate_version():
"""Returns the version of the library."""
# Fall back to NPM's installed package version, and assume the sources
# are dirty since the build scripts are being run at all after install.
try:
return git_version()
except RuntimeError:
# If there is an error in |git_version|, ignore it and try NPM. If there
# is an error with NPM, propagate the error.
return npm_version(is_dirty=True)
def get_closure_base_js_path():
return os.path.join(get_source_base(),
'node_modules', 'google-closure-library', 'closure', 'goog', 'base.js')
def get_all_js_files(*path_components):
"""Get all JavaScript file paths recursively from the given path components.
Args:
*path_components: The components of the path to search. Joining is handling
internally according to os path semantics.
Returns:
An array of absolute paths to all JS files.
"""
match = re.compile(r'.*\.js$')
return get_all_files(
os.path.join(get_source_base(), *path_components), match)
def get_all_files(dir_path, exp=None):
"""Get all file paths recursively within the given path.
This optionally will filter the output using the given regex.
Args:
dir_path: The string path to search.
exp: A regex to match, can be None.
Returns:
An array of absolute paths to all the files.
"""
ret = []
for root, _, files in os.walk(dir_path):
for f in files:
if not exp or exp.match(f):
ret.append(os.path.join(root, f))
ret.sort()
return ret
def get_node_binary(module_name, bin_name=None):
"""Returns an array to be used in the command-line execution of a node binary.
For example, this may return ['eslint'] (global install)
or ['node', 'path/to/node_modules/eslint/bin/eslint.js'] (local install).
Arguments:
module_name: A string, the name of the module.
bin_name: An optional string, the name of the binary, which defaults to
module_name if not provided.
Returns:
An array of strings which form the command-line to call the binary.
"""
if not bin_name:
bin_name = module_name
# Check local modules first.
base = get_source_base()
path = os.path.join(base, 'node_modules', module_name)
if os.path.isdir(path):
json_path = os.path.join(path, 'package.json')
package_data = json.load(open_file(json_path, 'r'))
bin_path = os.path.join(path, package_data['bin'][bin_name])
return ['node', bin_path]
# Not found locally, assume it can be found in os.environ['PATH'].
return [bin_name]
class InDir(object):
"""A Context Manager that changes directories temporarily and safely."""
def __init__(self, path):
self.new_path = path
def __enter__(self):
self.old_path = os.getcwd()
os.chdir(self.new_path)
def __exit__(self, type, value, traceback):
os.chdir(self.old_path)
def update_node_modules():
"""Updates the node modules using 'npm', if they have not already been
updated recently enough."""
if not _modules_need_update():
return True
base = cygwin_safe_path(get_source_base())
cmd = 'npm.cmd' if is_windows() else 'npm'
# Check the version of npm.
version = execute_get_output([cmd, '-v']).decode('utf8')
if _parse_version(version) < _parse_version('5.0.0'):
logging.error('npm version is too old, please upgrade. e.g.:')
logging.error(' npm install -g npm')
return False
# Update the modules.
# Actually change directories instead of using npm --prefix.
# See npm/npm#17027 and google/shaka-player#776 for more details.
with InDir(base):
# npm update seems to be the wrong thing in npm v5, so use install.
# See google/shaka-player#854 for more details.
execute_get_output([cmd, 'install'])
# Update the timestamp of the file that tracks when we last updated.
open(_node_modules_last_update_path(), 'wb').close()
return True
def run_main(main):
"""Executes the given function with the current command-line arguments.
This calls exit with the return value. This ignores keyboard interrupts.
Args:
main: The main function to call.
"""
logging.getLogger().setLevel(logging.INFO)
fmt = '[%(levelname)s] %(message)s'
logging.basicConfig(format=fmt)
try:
sys.exit(main(sys.argv[1:]))
except KeyboardInterrupt:
if os.environ.get('RAISE_INTERRUPT'):
raise
print(file=sys.stderr) # Clear the current line that has ^C on it.
logging.error('Keyboard interrupt')
sys.exit(1)