Commit 09ab75a5 authored by Avi Drissman's avatar Avi Drissman Committed by Commit Bot

Mac packaging: make distributions that can share app bundles do so

Bug: 1142508
Change-Id: I8e310994740653c0d5174528b5b0ef36e9b0e32b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2499881Reviewed-by: default avatarRobert Sesek <rsesek@chromium.org>
Reviewed-by: default avatarMark Mentovai <mark@chromium.org>
Commit-Queue: Avi Drissman <avi@chromium.org>
Cr-Commit-Position: refs/heads/master@{#822666}
parent 5beedd01
......@@ -227,6 +227,18 @@ class Distribution(object):
self.package_as_dmg = package_as_dmg
self.package_as_pkg = package_as_pkg
def brandless_copy(self):
"""Derives and returns a copy of this Distribution object, identical
except for not having a branding code.
This is useful in the case where a non-branded app bundle needs to be
created with otherwise the same configuration.
"""
return Distribution(self.channel, None, self.app_name_fragment,
self.packaging_name_fragment, self.product_dirname,
self.creator_code, self.channel_customize,
self.package_as_dmg, self.package_as_pkg)
def to_config(self, base_config):
"""Produces a derived |config.CodeSignConfig| for the Distribution.
......
......@@ -15,6 +15,22 @@ import plistlib
from . import commands, model, modification, notarize, parts, signing
def _include_branding_code_in_app(dist):
"""Returns whether to omit the branding code from the Chrome .app bundle.
If a distribution is packaged in a PKG (but is not also packaged in a DMG),
then the brand code is carried in the PKG script, and should not be added to
the .app bundle's Info.plist.
Args:
dist: The |model.Distribution|.
Returns:
Whether to include the branding code in the app bundle.
"""
return dist.package_as_dmg or not dist.package_as_pkg
def _customize_and_sign_chrome(paths, dist_config, dest_dir, signed_frameworks):
"""Does channel customization and signing of a Chrome distribution. The
resulting app bundle is moved into |dest_dir|.
......@@ -38,8 +54,15 @@ def _customize_and_sign_chrome(paths, dist_config, dest_dir, signed_frameworks):
os.path.join(paths.input, dist_config.base_config.app_dir), paths.work)
# Customize the app bundle.
modification.customize_distribution(paths, dist_config.distribution,
dist_config)
customization_dist = dist_config.distribution
customization_dist_config = dist_config
if not _include_branding_code_in_app(customization_dist):
customization_dist = dist_config.distribution.brandless_copy()
customization_dist_config = customization_dist.to_config(
dist_config.base_config)
modification.customize_distribution(paths, customization_dist,
customization_dist_config)
work_dir_framework_path = os.path.join(paths.work,
dist_config.framework_dir)
......@@ -171,23 +194,25 @@ def _component_property_path(paths, dist_config):
return component_property_path
def _productbuild_distribution_path(paths, dist_config, component_pkg_path):
def _productbuild_distribution_path(app_paths, pkg_paths, dist_config,
component_pkg_path):
"""Creates a distribution XML file for use by `productbuild`. This copies
the OS requirement from the copy of Chrome being packaged.
Args:
paths: A |model.Paths| object.
app_paths: A |model.Paths| object for the app.
pkg_paths: A |model.Paths| object for the pkg files.
dist_config: The |config.CodeSignConfig| object.
component_pkg_path: The path to the existing component .pkg file.
Returns:
The path to the distribution file.
"""
distribution_path = os.path.join(paths.work,
distribution_path = os.path.join(pkg_paths.work,
'{}.dist'.format(dist_config.app_product))
app_plist_path = os.path.join(paths.work, dist_config.app_dir, 'Contents',
'Info.plist')
app_plist_path = os.path.join(app_paths.work, dist_config.app_dir,
'Contents', 'Info.plist')
with commands.PlistContext(app_plist_path) as app_plist:
# For now, restrict installation to only the boot volume (the <domains/>
# tag) to simplify the Keystone installation.
......@@ -248,56 +273,63 @@ def _package_and_sign_pkg(paths, dist_config):
"""
assert dist_config.installer_identity
# Because several .pkg distributions might be built from the same underlying
# .app, separate the .pkg construction into its own work directory.
with commands.WorkDirectory(paths) as pkg_paths:
# There are two .pkg files to be built:
# 1. The inner component package (which is the one that can contain things
# like postinstall scripts). This is built with `pkgbuild`.
# 2. The outer distribution package (which is the installable thing that
# has pre-install requirements). This is built with `productbuild`.
# 1. The inner component package (which is the one that can contain
# things like postinstall scripts). This is built with `pkgbuild`.
# 2. The outer distribution package (which is the installable thing
# that has pre-install requirements). This is built with
# `productbuild`.
## The component package.
# Because the component package is built using the --root option, copy the
# .app into a directory by itself, as `pkgbuild` archives the entire
# Because the component package is built using the --root option, copy
# the .app into a directory by itself, as `pkgbuild` archives the entire
# directory specified as the root directory.
root_directory = os.path.join(paths.work, 'payload')
root_directory = os.path.join(pkg_paths.work, 'payload')
commands.make_dir(root_directory)
app_path = os.path.join(paths.work, dist_config.app_dir)
new_app_path = os.path.join(root_directory, dist_config.app_dir)
commands.copy_files(app_path, root_directory)
# The spaces are removed from |dist_config.app_product| for the component
# package path due to a bug in Installer.app that causes the "Show Files"
# window to be blank if there is a space in a component package name.
# https://stackoverflow.com/questions/43031272/
# The spaces are removed from |dist_config.app_product| for the
# component package path due to a bug in Installer.app that causes the
# "Show Files" window to be blank if there is a space in a component
# package name. https://stackoverflow.com/questions/43031272/
component_pkg_name = '{}.pkg'.format(dist_config.app_product).replace(
' ', '')
component_pkg_path = os.path.join(paths.work, component_pkg_name)
component_property_path = _component_property_path(paths, dist_config)
scripts_path = _create_pkgbuild_scripts(paths, dist_config)
component_pkg_path = os.path.join(pkg_paths.work, component_pkg_name)
component_property_path = _component_property_path(
pkg_paths, dist_config)
scripts_path = _create_pkgbuild_scripts(pkg_paths, dist_config)
commands.run_command([
'pkgbuild', '--root', root_directory, '--component-plist',
component_property_path, '--identifier', dist_config.base_bundle_id,
'--version', dist_config.version, '--install-location', '/Applications',
'--scripts', scripts_path, component_pkg_path
'--version', dist_config.version, '--install-location',
'/Applications', '--scripts', scripts_path, component_pkg_path
])
## The distribution package.
distribution_path = _productbuild_distribution_path(paths, dist_config,
component_pkg_path)
distribution_path = _productbuild_distribution_path(
paths, pkg_paths, dist_config, component_pkg_path)
product_pkg_path = os.path.join(
paths.output, '{}.pkg'.format(dist_config.packaging_basename))
pkg_paths.output, '{}.pkg'.format(dist_config.packaging_basename))
command = [
'productbuild', '--identifier', dist_config.base_bundle_id, '--version',
dist_config.version, '--distribution', distribution_path,
'--package-path', paths.work, '--sign', dist_config.installer_identity
'productbuild', '--identifier', dist_config.base_bundle_id,
'--version', dist_config.version, '--distribution',
distribution_path, '--package-path', pkg_paths.work, '--sign',
dist_config.installer_identity
]
if dist_config.notary_user:
# Assume if the config has notary authentication information that the
# products will be notarized, which requires a secure timestamp.
# Assume if the config has notary authentication information that
# the products will be notarized, which requires a secure
# timestamp.
command.append('--timestamp')
command.append(product_pkg_path)
commands.run_command(command)
......@@ -442,20 +474,40 @@ def _package_installer_tools(paths, config):
cwd=paths.work)
def _intermediate_work_dir_name(dist_config):
def _intermediate_work_dir_name(dist):
"""Returns the name of an intermediate work directory for a distribution.
All distributions that can share the same app bundle share the intermediate
work directory.
Just about any customization in the distribution will require it to have its
own app bundle. However, if a distribution is packaged in a PKG (but is not
also packaged in a DMG), then the brand code is carried in the PKG script,
and the distribution can share the app bundle of a different distribution
which is unbranded but for which all the other customizations match.
Args:
dist_config: A |config.CodeSignConfig| for the |model.Distribution|.
dist: The |model.Distribution|.
Returns:
The work directory name to use.
"""
if dist_config.distribution.branding_code:
return '{}-{}'.format(dist_config.packaging_basename,
dist_config.distribution.branding_code)
customizations = []
if dist.channel_customize:
customizations.append('sxs')
if dist.channel:
customizations.append(dist.channel)
else:
customizations.append('stable')
if dist.app_name_fragment:
customizations.append(dist.app_name_fragment)
if dist.product_dirname:
customizations.append(dist.product_dirname.replace('/', ' '))
if dist.creator_code:
customizations.append(dist.creator_code)
if dist.branding_code and _include_branding_code_in_app(dist):
customizations.append(dist.branding_code)
return dist_config.packaging_basename
return '-'.join(customizations)
def sign_all(orig_paths,
......@@ -470,9 +522,10 @@ def sign_all(orig_paths,
Args:
orig_paths: A |model.Paths| object.
config: The |config.CodeSignConfig| object.
package_dmg: If True, the signed application bundle will be packaged
into a DMG, which will also be signed. If False, the signed app
bundle will be copied to |paths.output|.
disable_packaging: Whether all packaging is disabled. If True, the
unpackaged signed app bundle will be copied to |paths.output|. If
False, the packaging specified in the distribution will be
performed.
do_notarization: If True, the signed application bundle will be sent for
notarization by Apple. The resulting notarization ticket will then
be stapled. If |package_dmg| is also True, the stapled application
......@@ -486,6 +539,7 @@ def sign_all(orig_paths,
# notarization requests.
uuids_to_config = {}
signed_frameworks = {}
created_app_bundles = set()
for dist in config.distributions:
if dist.branding_code in skip_brands:
continue
......@@ -502,8 +556,17 @@ def sign_all(orig_paths,
else:
dest_dir = notary_paths.work
dest_dir = os.path.join(
dest_dir, _intermediate_work_dir_name(dist_config))
dest_dir = os.path.join(dest_dir,
_intermediate_work_dir_name(dist))
# Different distributions might share the same underlying app
# bundle, and if they do, then the _intermediate_work_dir_name
# function will return the same value. Skip creating another app
# bundle if that is the case.
if dest_dir in created_app_bundles:
continue
created_app_bundles.add(dest_dir)
_customize_and_sign_chrome(paths, dist_config, dest_dir,
signed_frameworks)
......@@ -527,7 +590,8 @@ def sign_all(orig_paths,
config):
dist_config = uuids_to_config[result]
dest_dir = os.path.join(
notary_paths.work, _intermediate_work_dir_name(dist_config))
notary_paths.work,
_intermediate_work_dir_name(dist_config.distribution))
_staple_chrome(notary_paths.replace_work(dest_dir), dist_config)
# After all apps are optionally notarized, package as required.
......@@ -539,8 +603,9 @@ def sign_all(orig_paths,
dist_config = dist.to_config(config)
paths = orig_paths.replace_work(
os.path.join(notary_paths.work,
_intermediate_work_dir_name(dist_config)))
os.path.join(
notary_paths.work,
_intermediate_work_dir_name(dist_config.distribution)))
if dist.package_as_dmg:
dmg_path = _package_and_sign_dmg(paths, dist_config)
......
......@@ -20,15 +20,15 @@ _get_work_dir.count = 0
def _component_property_path(paths, dist_config):
return '/$W/App Product.plist'
return '/$W_1/App Product.plist'
def _productbuild_distribution_path(p, d, c):
return '/$W/App Product.dist'
def _productbuild_distribution_path(ap, pp, d, c):
return '/$W_1/App Product.dist'
def _create_pkgbuild_scripts(p, d):
return '/$W/scripts'
return '/$W_1/scripts'
def _read_plist(p):
......@@ -361,7 +361,7 @@ framework dir is 'App Product.app/Contents/Frameworks/Product Framework.framewor
self.assertEqual(
'/$W/App Product.dist',
pipeline._productbuild_distribution_path(paths, dist_config,
pipeline._productbuild_distribution_path(paths, paths, dist_config,
component_pkg_path))
manager.assert_has_calls([
......@@ -439,15 +439,15 @@ framework dir is 'App Product.app/Contents/Frameworks/Product Framework.framewor
pkgbuild_args = run_commands[0][1][0]
productbuild_args = run_commands[1][1][0]
self.assertEqual('/$W/payload',
self.assertEqual('/$W_1/payload',
_get_adjacent_item(pkgbuild_args, '--root'))
self.assertEqual('/$W/App Product.plist',
self.assertEqual('/$W_1/App Product.plist',
_get_adjacent_item(pkgbuild_args, '--component-plist'))
self.assertEqual('test.signing.bundle_id',
_get_adjacent_item(pkgbuild_args, '--identifier'))
self.assertEqual('99.0.9999.99',
_get_adjacent_item(pkgbuild_args, '--version'))
self.assertEqual('/$W/scripts',
self.assertEqual('/$W_1/scripts',
_get_adjacent_item(pkgbuild_args, '--scripts'))
self.assertEqual('test.signing.bundle_id',
......@@ -455,10 +455,10 @@ framework dir is 'App Product.app/Contents/Frameworks/Product Framework.framewor
self.assertEqual('99.0.9999.99',
_get_adjacent_item(productbuild_args, '--version'))
self.assertEqual(
'/$W/App Product.dist',
'/$W_1/App Product.dist',
_get_adjacent_item(productbuild_args, '--distribution'))
self.assertEqual(
'/$W', _get_adjacent_item(productbuild_args, '--package-path'))
'/$W_1', _get_adjacent_item(productbuild_args, '--package-path'))
self.assertEqual('[INSTALLER-IDENTITY]',
_get_adjacent_item(productbuild_args, '--sign'))
......@@ -531,15 +531,15 @@ framework dir is 'App Product.app/Contents/Frameworks/Product Framework.framewor
pkgbuild_args = run_commands[0][1][0]
productbuild_args = run_commands[1][1][0]
self.assertEqual('/$W/payload',
self.assertEqual('/$W_1/payload',
_get_adjacent_item(pkgbuild_args, '--root'))
self.assertEqual('/$W/App Product.plist',
self.assertEqual('/$W_1/App Product.plist',
_get_adjacent_item(pkgbuild_args, '--component-plist'))
self.assertEqual('test.signing.bundle_id',
_get_adjacent_item(pkgbuild_args, '--identifier'))
self.assertEqual('99.0.9999.99',
_get_adjacent_item(pkgbuild_args, '--version'))
self.assertEqual('/$W/scripts',
self.assertEqual('/$W_1/scripts',
_get_adjacent_item(pkgbuild_args, '--scripts'))
self.assertEqual('test.signing.bundle_id',
......@@ -547,10 +547,10 @@ framework dir is 'App Product.app/Contents/Frameworks/Product Framework.framewor
self.assertEqual('99.0.9999.99',
_get_adjacent_item(productbuild_args, '--version'))
self.assertEqual(
'/$W/App Product.dist',
'/$W_1/App Product.dist',
_get_adjacent_item(productbuild_args, '--distribution'))
self.assertEqual(
'/$W', _get_adjacent_item(productbuild_args, '--package-path'))
'/$W_1', _get_adjacent_item(productbuild_args, '--package-path'))
self.assertEqual('[INSTALLER-IDENTITY]',
_get_adjacent_item(productbuild_args, '--sign'))
......@@ -795,21 +795,20 @@ class TestSignAll(unittest.TestCase):
manager.assert_has_calls([
# First customize the distribution and sign it.
mock.call._customize_and_sign_chrome(
mock.ANY, mock.ANY, '/$W_1/AppProduct-99.0.9999.99', mock.ANY),
mock.call._customize_and_sign_chrome(mock.ANY, mock.ANY,
'/$W_1/stable', mock.ANY),
# Prepare the app for notarization.
mock.call.run_command([
'zip', '--recurse-paths', '--symlinks', '--quiet',
'/$W_1/AppProduct-99.0.9999.99.zip', 'App Product.app'
],
cwd='/$W_1/AppProduct-99.0.9999.99'),
cwd='/$W_1/stable'),
mock.call.submit('/$W_1/AppProduct-99.0.9999.99.zip', mock.ANY),
mock.call.shutil.rmtree('/$W_2'),
mock.call.wait_for_results({app_uuid: None}.keys(), mock.ANY),
mock.call._staple_chrome(
self.paths.replace_work('/$W_1/AppProduct-99.0.9999.99'),
mock.ANY),
self.paths.replace_work('/$W_1/stable'), mock.ANY),
# Make the DMG.
mock.call._package_and_sign_dmg(mock.ANY, mock.ANY),
......@@ -854,21 +853,20 @@ class TestSignAll(unittest.TestCase):
manager.assert_has_calls([
# First customize the distribution and sign it.
mock.call._customize_and_sign_chrome(
mock.ANY, mock.ANY, '/$W_1/AppProduct-99.0.9999.99', mock.ANY),
mock.call._customize_and_sign_chrome(mock.ANY, mock.ANY,
'/$W_1/stable', mock.ANY),
# Prepare the app for notarization.
mock.call.run_command([
'zip', '--recurse-paths', '--symlinks', '--quiet',
'/$W_1/AppProduct-99.0.9999.99.zip', 'App Product.app'
],
cwd='/$W_1/AppProduct-99.0.9999.99'),
cwd='/$W_1/stable'),
mock.call.submit('/$W_1/AppProduct-99.0.9999.99.zip', mock.ANY),
mock.call.shutil.rmtree('/$W_2'),
mock.call.wait_for_results({app_uuid: None}.keys(), mock.ANY),
mock.call._staple_chrome(
self.paths.replace_work('/$W_1/AppProduct-99.0.9999.99'),
mock.ANY),
self.paths.replace_work('/$W_1/stable'), mock.ANY),
# Make the DMG.
mock.call._package_and_sign_pkg(mock.ANY, mock.ANY),
......@@ -916,21 +914,20 @@ class TestSignAll(unittest.TestCase):
manager.assert_has_calls([
# First customize the distribution and sign it.
mock.call._customize_and_sign_chrome(
mock.ANY, mock.ANY, '/$W_1/AppProduct-99.0.9999.99', mock.ANY),
mock.call._customize_and_sign_chrome(mock.ANY, mock.ANY,
'/$W_1/stable', mock.ANY),
# Prepare the app for notarization.
mock.call.run_command([
'zip', '--recurse-paths', '--symlinks', '--quiet',
'/$W_1/AppProduct-99.0.9999.99.zip', 'App Product.app'
],
cwd='/$W_1/AppProduct-99.0.9999.99'),
cwd='/$W_1/stable'),
mock.call.submit('/$W_1/AppProduct-99.0.9999.99.zip', mock.ANY),
mock.call.shutil.rmtree('/$W_2'),
mock.call.wait_for_results({app_uuid: None}.keys(), mock.ANY),
mock.call._staple_chrome(
self.paths.replace_work('/$W_1/AppProduct-99.0.9999.99'),
mock.ANY),
self.paths.replace_work('/$W_1/stable'), mock.ANY),
# Make the DMG, and submit for notarization.
mock.call._package_and_sign_dmg(mock.ANY, mock.ANY),
......@@ -967,21 +964,20 @@ class TestSignAll(unittest.TestCase):
manager.assert_has_calls([
# First customize the distribution and sign it.
mock.call._customize_and_sign_chrome(
mock.ANY, mock.ANY, '/$W_1/AppProduct-99.0.9999.99', mock.ANY),
mock.call._customize_and_sign_chrome(mock.ANY, mock.ANY,
'/$W_1/stable', mock.ANY),
# Prepare the app for notarization.
mock.call.run_command([
'zip', '--recurse-paths', '--symlinks', '--quiet',
'/$W_1/AppProduct-99.0.9999.99.zip', 'App Product.app'
],
cwd='/$W_1/AppProduct-99.0.9999.99'),
cwd='/$W_1/stable'),
mock.call.submit('/$W_1/AppProduct-99.0.9999.99.zip', mock.ANY),
mock.call.shutil.rmtree('/$W_2'),
mock.call.wait_for_results({app_uuid: None}.keys(), mock.ANY),
mock.call._staple_chrome(
self.paths.replace_work('/$W_1/AppProduct-99.0.9999.99'),
mock.ANY),
self.paths.replace_work('/$W_1/stable'), mock.ANY),
mock.call.shutil.rmtree('/$W_1'),
# Package the installer tools.
......@@ -1003,8 +999,8 @@ class TestSignAll(unittest.TestCase):
manager.assert_has_calls([
# First customize the distribution and sign it.
mock.call._customize_and_sign_chrome(
mock.ANY, mock.ANY, '/$W_1/AppProduct-99.0.9999.99', mock.ANY),
mock.call._customize_and_sign_chrome(mock.ANY, mock.ANY,
'/$W_1/stable', mock.ANY),
mock.call.shutil.rmtree('/$W_2'),
# Make the DMG.
......@@ -1027,8 +1023,7 @@ class TestSignAll(unittest.TestCase):
manager.assert_has_calls([
# First customize the distribution and sign it.
mock.call._customize_and_sign_chrome(mock.ANY, mock.ANY,
'/$O/AppProduct-99.0.9999.99',
mock.ANY),
'/$O/stable', mock.ANY),
mock.call.shutil.rmtree('/$W_2'),
mock.call.shutil.rmtree('/$W_1'),
......@@ -1071,42 +1066,32 @@ class TestSignAll(unittest.TestCase):
pipeline.sign_all(self.paths, config, do_notarization=False)
self.assertEqual(1, kwargs['_package_installer_tools'].call_count)
self.assertEqual(4, kwargs['_customize_and_sign_chrome'].call_count)
self.assertEqual(3, kwargs['_customize_and_sign_chrome'].call_count)
manager.assert_has_calls([
# Customizations.
mock.call._customize_and_sign_chrome(
mock.ANY, mock.ANY, '/$W_1/AppProduct-99.0.9999.99', mock.ANY),
mock.call._customize_and_sign_chrome(mock.ANY, mock.ANY,
'/$W_1/stable', mock.ANY),
mock.call.shutil.rmtree('/$W_2'),
mock.call._customize_and_sign_chrome(
mock.ANY, mock.ANY, '/$W_1/AppProduct-99.0.9999.99-ForCows-MOO',
mock.ANY),
mock.call._customize_and_sign_chrome(mock.ANY, mock.ANY,
'/$W_1/stable-MOO', mock.ANY),
mock.call.shutil.rmtree('/$W_3'),
mock.call._customize_and_sign_chrome(
mock.ANY, mock.ANY, '/$W_1/AppProduct-99.0.9999.99-ForDogs-ARF',
mock.ANY),
mock.call.shutil.rmtree('/$W_4'),
mock.call._customize_and_sign_chrome(
mock.ANY, mock.ANY,
'/$W_1/AppProduct-99.0.9999.99-ForDogcows-MOOF', mock.ANY),
mock.call._customize_and_sign_chrome(mock.ANY, mock.ANY,
'/$W_1/stable-MOOF', mock.ANY),
mock.call.shutil.rmtree('/$W_5'),
# Packaging and signing.
mock.call._package_and_sign_dmg(
self.paths.replace_work('/$W_1/AppProduct-99.0.9999.99'),
mock.ANY),
self.paths.replace_work('/$W_1/stable'), mock.ANY),
mock.call._package_and_sign_dmg(
self.paths.replace_work(
'/$W_1/AppProduct-99.0.9999.99-ForCows-MOO'), mock.ANY),
self.paths.replace_work('/$W_1/stable-MOO'), mock.ANY),
mock.call._package_and_sign_pkg(
self.paths.replace_work(
'/$W_1/AppProduct-99.0.9999.99-ForDogs-ARF'), mock.ANY),
self.paths.replace_work('/$W_1/stable'), mock.ANY),
mock.call._package_and_sign_dmg(
self.paths.replace_work(
'/$W_1/AppProduct-99.0.9999.99-ForDogcows-MOOF'), mock.ANY),
self.paths.replace_work('/$W_1/stable-MOOF'), mock.ANY),
mock.call._package_and_sign_pkg(
self.paths.replace_work(
'/$W_1/AppProduct-99.0.9999.99-ForDogcows-MOOF'), mock.ANY),
self.paths.replace_work('/$W_1/stable-MOOF'), mock.ANY),
mock.call.shutil.rmtree('/$W_1'),
# Finally the installer tools.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment