blob: 9503f20f24b77c720cb8721b2be85156ab32b5db [file] [log] [blame]
# Copyright 2017 The Cobalt Authors. All Rights Reserved.
#
# 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.
"""Generate a xb1 package.
Generates a xb1 package from the loose files.
Called by package_application.py
"""
import argparse
import logging
import os
import platform
import shutil
import subprocess
import sys
from xml.etree import ElementTree as ET
import zipfile
from starboard.tools import package
_INTERNAL_CERT_PATH = os.path.join(
os.path.dirname(os.path.realpath(__file__)), os.pardir, os.pardir,
os.pardir, 'internal', 'starboard', 'xb1', 'cert', 'youtube.pfx')
_EXTERNAL_CERT_PATH = os.path.join(
os.path.dirname(os.path.realpath(__file__)), os.pardir, os.pardir,
os.pardir, 'starboard', 'xb1', 'cert', 'cobalt.pfx')
_APPX_MANIFEST_XML_NAMESPACE = \
'http://schemas.microsoft.com/appx/manifest/foundation/windows10'
_NAMESPACE_DICT = {'appx': _APPX_MANIFEST_XML_NAMESPACE}
_PUBLISHER_XPATH = './appx:Identity[@Publisher]'
_PRODUCT_APPX_NAME = {
'cobalt': 'cobalt',
'youtube': 'cobalt',
'mainappbeta': 'mainappbeta',
'youtubetv': 'youtubetv'
}
_PRODUCT_CERT_PATH = {
'cobalt': _EXTERNAL_CERT_PATH,
'youtube': _INTERNAL_CERT_PATH,
'mainappbeta': _INTERNAL_CERT_PATH,
'youtubetv': _INTERNAL_CERT_PATH,
}
_DEFAULT_SDK_BIN_DIR = 'C:\\Program Files (x86)\\Windows Kits\\10\\bin'
_DEFAULT_WIN_SDK_VERSION = '10.0.22000.0'
_SOURCE_SPLASH_SCREEN_SUB_PATH = os.path.join('internal', 'cobalt', 'browser',
'splash_screen')
# The splash screen file referenced in starboard/xb1/shared/configuration.cc
# must be in appx/content/data/web/ to be found at runtime.
_DESTINATION__SPLASH_SCREEN_SUB_PATH = os.path.join('appx', 'content', 'data',
'web', 'splash_screen')
_SPLASH_SCREEN_FILE = {
'cobalt': '',
'youtube': 'youtube_splash_screen.html',
'youtubetv': 'ytv_splash_screen.html',
'mainappbeta': 'youtube_splash_screen.html',
}
def _SelectBestPath(os_var_name, path):
if os_var_name in os.environ:
return os.environ[os_var_name]
if os.path.exists(path):
return path
new_path = path.replace('Program Files (x86)', 'mappedProgramFiles')
if os.path.exists(new_path):
return new_path
return path
def _GetSourceSplashScreenDir():
# Relative to this file, the path is
# "../../../internal/cobalt/browser/splash_screen".
src_dir = os.path.join(
os.path.dirname(__file__), os.pardir, os.pardir, os.pardir)
return os.path.join(src_dir, _SOURCE_SPLASH_SCREEN_SUB_PATH)
class Package(package.PackageBase):
"""A class representing an installable UWP Appx package."""
@classmethod
def AddArguments(cls, arg_parser):
"""Add xb1-specific command-line arguments to the ArgumentParser."""
super(Package, cls).AddArguments(arg_parser)
arg_parser.add_argument(
'-p',
'--publisher',
dest='publisher',
default=None,
help='Publisher Identity in Package.')
@classmethod
def ExtractArguments(cls, options):
kwargs = super(Package, cls).ExtractArguments(options)
kwargs['publisher'] = options.publisher
kwargs['product'] = options.product
return kwargs
def _GetDestinationSplashScreenDir(self):
return os.path.join(self.source_dir, _DESTINATION__SPLASH_SCREEN_SUB_PATH)
def _CleanSplashScreenDir(self):
splash_screen_dir = self._GetDestinationSplashScreenDir()
if not os.path.exists(splash_screen_dir):
return
shutil.rmtree(splash_screen_dir)
def _CopySplashScreen(self):
splash_screen_dir = self._GetDestinationSplashScreenDir()
# Create the splash screen directory if necessary.
if not os.path.exists(splash_screen_dir):
try:
os.makedirs(splash_screen_dir)
except OSError as e:
raise RuntimeError(f'Failed to create {splash_screen_dir}: {e}') from e
# Copy the correct splash screen for the current product into content.
splash_screen_file = _SPLASH_SCREEN_FILE[self.product]
src_splash_screen_dir = _GetSourceSplashScreenDir()
src_splash_screen_file = os.path.join(src_splash_screen_dir,
splash_screen_file)
if not os.path.exists(src_splash_screen_file):
logging.error('Failed to find splash screen file in source : %s',
src_splash_screen_file)
return
shutil.copy(src_splash_screen_file, splash_screen_dir)
@classmethod
def SupportedPlatforms(cls):
if platform.system() == 'Windows':
return ['xb1']
else:
return []
def __init__(self, publisher, product, **kwargs):
windows_sdk_bin_dir = _SelectBestPath('WindowsSdkBinPath',
_DEFAULT_SDK_BIN_DIR)
self.windows_sdk_host_tools = os.path.join(windows_sdk_bin_dir,
_DEFAULT_WIN_SDK_VERSION, 'x64')
self.publisher = publisher
self.product = product
super().__init__(**kwargs)
if not os.path.exists(self.source_dir):
raise RuntimeError(
f'Package source directory {self.source_dir} not found.')
logging.debug('Building package')
if self.publisher:
self._UpdateAppxManifestPublisher(publisher)
# Remove any previous splash screen from content.
self._CleanSplashScreenDir()
# Copy the correct splash screen into content.
self._CopySplashScreen()
self.package = self._BuildPackage()
if not os.path.exists(self.package):
raise RuntimeError(f'Package file {self.package} does not exist.')
appxsym_out_file = self.appxsym_location
with zipfile.ZipFile(appxsym_out_file, 'w', zipfile.ZIP_DEFLATED) as z:
if os.path.isfile(os.path.join(self.source_dir, 'cobalt.exe.pdb')):
z.write(os.path.join(self.source_dir, 'cobalt.exe.pdb'))
with zipfile.ZipFile(self.appxupload_location, 'w',
zipfile.ZIP_DEFLATED) as z:
z.write(appxsym_out_file)
z.write(self.appx_location)
logging.info('Finished.')
def _UpdateAppxManifestPublisher(self, publisher):
logging.info('Updating Appx with publisher [%s]', publisher)
tree = ET.parse(self.appxmanifest_location)
for element in tree.findall(_PUBLISHER_XPATH, _NAMESPACE_DICT):
assert 'Publisher' in element.attrib
element.attrib['Publisher'] = publisher
tree.write(self.appxmanifest_location)
def Install(self, targets=None):
# TODO: implement
pass
@property
def appx_folder_location(self):
product_locations = {
'cobalt': 'appx',
'youtube': 'appx',
'mainappbeta': 'mainappbeta-appx',
'youtubetv': 'youtubetv-appx'
}
return os.path.join(self.source_dir, product_locations[self.product])
@property
def appx_location(self):
return os.path.join(self.output_dir,
_PRODUCT_APPX_NAME[self.product] + '.appx')
@property
def appxsym_location(self):
return os.path.join(self.output_dir,
_PRODUCT_APPX_NAME[self.product] + '.appxsym')
@property
def appxupload_location(self):
return os.path.join(self.output_dir,
_PRODUCT_APPX_NAME[self.product] + '.appxupload')
@property
def appxmanifest_location(self):
return os.path.join(self.appx_folder_location, 'AppxManifest.xml')
def _BuildPackage(self):
"""Build the package for XB1.
Raises:
RuntimeError: if packaging or verification (if requested) fails.
Returns:
path to package location.
"""
logging.info('Creating the package at: %s', self.appx_location)
tools_path = self.windows_sdk_host_tools
makeappx_args = [
os.path.join(tools_path, 'makeappx.exe'), 'pack', '/l', '/o', '/d',
self.appx_folder_location, '/p', self.appx_location
]
logging.info('Running %s', ' '.join(makeappx_args))
subprocess.check_call(makeappx_args)
cert_path = _PRODUCT_CERT_PATH[self.product]
try:
signtool_args = [
os.path.join(tools_path, 'signtool.exe'), 'sign', '/fd', 'SHA256',
'/a', '/f', cert_path, '/v', '/debug', self.appx_location
]
logging.info('Running %s', ' '.join(signtool_args))
subprocess.check_call(signtool_args)
except subprocess.CalledProcessError as sub_err:
if '0x80070070' in str(sub_err):
print('\n\n*** ERROR CAUSED BY LOW DISK SPACE!! ***\n\n')
raise # Rethrow original error with original stack trace.
return self.appx_location
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
'-s',
'--source',
required=True,
help='Source directory from which to create a package.')
parser.add_argument(
'-o',
'--output',
default=os.path.join(
os.path.dirname(os.path.realpath(__file__)), 'package'),
help='Output directory to place the packaged app. Defaults to ./package/')
parser.add_argument(
'-p',
'--product',
default='cobalt',
help=(
'Product name. This must be one of [cobalt, youtube, youtubetv,'
'mainappbeta]. Any builds that are not internal to YouTube should use'
'cobalt.'))
args, _ = parser.parse_known_args()
if not os.path.exists(args.output):
os.makedirs(args.output)
Package(
publisher=None,
product=args.product,
source_dir=args.source,
output_dir=args.output)
if __name__ == '__main__':
sys.exit(main())