Commit ce26fb05 authored by Avi Drissman's avatar Avi Drissman Committed by Commit Bot

[pkg] Package a postinstall script.

The included script installs Keystone, and then registers
Chrome with it.

Bug: 1022416
Change-Id: I887b54e0872b0a065cd1ffe17463c06daf460594
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1930144Reviewed-by: default avatarMark Mentovai <mark@chromium.org>
Reviewed-by: default avatarRobert Sesek <rsesek@chromium.org>
Commit-Queue: Avi Drissman <avi@chromium.org>
Cr-Commit-Position: refs/heads/master@{#721673}
parent 7cd6dc50
......@@ -81,6 +81,7 @@ copy("copies") {
"dmgdiffer.sh",
"notarize_thing.py",
"pkg-dmg",
"pkg_postinstall.in",
"sign_chrome.py",
]
......
#!/bin/bash -p
# Copyright 2019 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
set -eu
# The parameters to a pkg postinstall script are not well documented, so for the
# reader's clarification:
#
# $1 is the top-level package path
# $2 is the target (installation) location
# $3 is the target (installation) volume
# $4 is the startup disk root
readonly INSTALLATION_PATH=${2}
readonly APP_DIR="@APP_DIR@"
readonly APP_PRODUCT="@APP_PRODUCT@"
readonly FRAMEWORK_DIR="@FRAMEWORK_DIR@"
# Uses "defaults read" to obtain the value of a key in a property list.
#
# See /chrome/installer/mac/keystone_install.sh for more details.
infoplist_read() {
__CFPREFERENCES_AVOID_DAEMON=1 defaults read "${@}"
}
# 1. Install Keystone.
echo "Installing Keystone."
readonly KS_REG_FRAMEWORK="${INSTALLATION_PATH}/${FRAMEWORK_DIR}/Frameworks/KeystoneRegistration.framework"
readonly KS_INSTALL="${KS_REG_FRAMEWORK}/Helpers/ksinstall"
readonly KS_ARCHIVE="${KS_REG_FRAMEWORK}/Resources/Keystone.tbz"
"${KS_INSTALL}" "--install=${KS_ARCHIVE}"
# 2. Create the brand file.
echo "Creating the brand file."
readonly CHROME_APP_LOCATION="${INSTALLATION_PATH}/${APP_DIR}"
readonly CHROME_APP_INFO_PLIST="${CHROME_APP_LOCATION}/Contents/Info.plist"
readonly BRAND_PLIST="/Library/Google/${APP_PRODUCT} Brand.plist"
readonly KS_BRAND_KEY="KSBrandID"
brand_code="$(infoplist_read "${CHROME_APP_INFO_PLIST}" \
"${KS_BRAND_KEY}")"
# Failure to write a brand file is less important than getting automatic updates
# configured.
brand_file_written="false"
defaults write "${BRAND_PLIST}" "${KS_BRAND_KEY}" \
-string "${brand_code}" \
&& chmod 644 "${BRAND_PLIST}" \
&& brand_file_written="true"
# 3. Register Chrome with Keystone.
echo "Registering Chrome with Keystone."
readonly KS_VERSION_KEY="KSVersion"
readonly KS_PRODUCT_KEY="KSProductID"
readonly KS_URL_KEY="KSUpdateURL"
readonly KS_CHANNEL_KEY="KSChannelID"
readonly KS_ADMIN="/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/Contents/MacOS/ksadmin"
product="$(infoplist_read "${CHROME_APP_INFO_PLIST}" \
"${KS_PRODUCT_KEY}")"
version="$(infoplist_read "${CHROME_APP_INFO_PLIST}" \
"${KS_VERSION_KEY}")"
update_url="$(infoplist_read "${CHROME_APP_INFO_PLIST}" \
"${KS_URL_KEY}")"
ksadmin_args=(
--register
--productid "${product}"
--version "${version}"
--xcpath "${CHROME_APP_LOCATION}"
--url "${update_url}"
--tag-path "${CHROME_APP_INFO_PLIST}"
--tag-key "${KS_CHANNEL_KEY}"
--version-path "${CHROME_APP_INFO_PLIST}"
--version-key "${KS_VERSION_KEY}"
)
if channel="$(infoplist_read "${CHROME_APP_INFO_PLIST}" \
"${KS_CHANNEL_KEY}")"; then
ksadmin_args+=(
--tag "${channel}"
)
fi
if [[ "${brand_file_written}" == "true" ]]; then
ksadmin_args+=(
--brand-path "${BRAND_PLIST}"
--brand-key "${KS_BRAND_KEY}"
)
fi
"${KS_ADMIN}" "${ksadmin_args[@]}"
......@@ -8,6 +8,7 @@ The commands module wraps operations that have side-effects.
import os
import plistlib
import shutil
import stat
import subprocess
import tempfile
......@@ -63,6 +64,22 @@ def write_file(path, contents):
f.write(contents)
def read_file(path):
with open(path, 'r') as f:
return f.read()
def set_executable(path):
"""Makes the file at the specified path executable.
Args:
path: The path to the file to make executable.
"""
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR
| stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH
| stat.S_IXOTH) # -rwxr-xr-x a.k.a. 0755
def run_command(args, **kwargs):
logger.info('Running command: %s', args)
subprocess.check_call(args, **kwargs)
......
......@@ -147,20 +147,18 @@ class TestCommands(unittest.TestCase):
self.assertFalse(commands.file_exists(orig_path))
self.assertTrue(commands.file_exists(new_path))
def test_write_file(self):
def test_read_write_file(self):
path = os.path.join(self.tempdir, 'file.txt')
data1 = 'hello world this is a test'
commands.write_file(path, data1)
with open(path, 'r') as f:
self.assertEqual(data1, f.read())
data1_read = commands.read_file(path)
self.assertEqual(data1, data1_read)
data2 = 'moo'
commands.write_file(path, data2)
with open(path, 'r') as f:
self.assertEqual(data2, f.read())
data2_read = commands.read_file(path)
self.assertEqual(data2, data2_read)
def test_run_command(self):
path = os.path.join(self.tempdir, 'touch.txt')
......
......@@ -118,6 +118,44 @@ def _staple_chrome(paths, dist_config):
notarize.staple(os.path.join(paths.work, part_path))
def _create_pkgbuild_scripts(paths, dist_config):
"""Creates a directory filled with scripts for use by `pkgbuild`, and copies
the postinstall script into this directory, customizing it along the way.
Args:
paths: A |model.Paths| object.
dist_config: The |config.CodeSignConfig| object.
Returns:
The path to the scripts directory.
"""
scripts_path = os.path.join(paths.work, 'scripts')
commands.make_dir(scripts_path)
packaging_dir = paths.packaging_dir(dist_config)
def do_substitutions(script):
substitutions = {
'@APP_DIR@': dist_config.app_dir,
'@APP_PRODUCT@': dist_config.app_product,
'@FRAMEWORK_DIR@': dist_config.framework_dir
}
for key, value in substitutions.items():
script = script.replace(key, value)
return script
postinstall_src_path = os.path.join(packaging_dir, 'pkg_postinstall.in')
postinstall_dest_path = os.path.join(scripts_path, 'postinstall')
postinstall = commands.read_file(postinstall_src_path)
postinstall = do_substitutions(postinstall)
commands.write_file(postinstall_dest_path, postinstall)
commands.set_executable(postinstall_dest_path)
return scripts_path
def _productbuild_distribution_path(paths, dist_config, component_pkg_path):
"""Creates a distribution XML file for use by `productbuild`. This specifies
that an x64 machine is required, and copies the OS requirement from the copy
......@@ -137,12 +175,15 @@ def _productbuild_distribution_path(paths, dist_config, component_pkg_path):
app_plist_path = os.path.join(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.
distribution_xml = """<?xml version="1.0" encoding="utf-8"?>
<installer-gui-script minSpecVersion="2">
<!-- Top-level info about the distribution. -->
<title>{app_product}</title>
<options customize="never" require-scripts="false" hostArchitectures="x86_64"/>
<domains enable_anywhere="false" enable_currentUserHome="false" enable_localSystem="true"/>
<volume-check>
<allowed-os-versions>
<os-version min="{minimum_system}"/>
......@@ -206,10 +247,12 @@ def _package_and_sign_pkg(paths, dist_config):
component_pkg_path = os.path.join(paths.work, component_pkg_name)
app_path = os.path.join(paths.work, dist_config.app_dir)
scripts_path = _create_pkgbuild_scripts(paths, dist_config)
commands.run_command([
'pkgbuild', '--identifier', dist_config.base_bundle_id, '--version',
dist_config.version, '--component', app_path, '--install-location',
'/Applications', component_pkg_path
'/Applications', '--scripts', scripts_path, component_pkg_path
])
## The product archive.
......
......@@ -23,10 +23,23 @@ def _productbuild_distribution_path(p, d, c):
return '$W/App Product.dist'
def _create_pkgbuild_scripts(p, d):
return '$W/scripts'
def _read_plist(p):
return {'LSMinimumSystemVersion': '10.19.7'}
def _read_file(p):
if p == '$I/Product Packaging/pkg_postinstall.in':
return """app dir is '@APP_DIR@'
app product is '@APP_PRODUCT@'
framework dir is '@FRAMEWORK_DIR@'"""
raise
def _get_adjacent_item(l, o):
"""Finds object |o| in collection |l| and returns the item at its index
plus 1.
......@@ -40,7 +53,7 @@ def _get_adjacent_item(l, o):
m: mock.DEFAULT
for m in ('move_file', 'copy_files',
'copy_dir_overwrite_and_count_changes', 'run_command',
'make_dir', 'shutil', 'write_file')
'make_dir', 'shutil', 'write_file', 'set_executable')
})
@mock.patch.multiple(
'signing.signing',
......@@ -250,6 +263,32 @@ class TestPipelineHelpers(unittest.TestCase):
mock.call('$W/App Product Canary.app')
])
@mock.patch('signing.commands.read_file', _read_file)
def test_create_pkgbuild_scripts(self, **kwargs):
manager = mock.Mock()
for attr in kwargs:
manager.attach_mock(kwargs[attr], attr)
dist = model.Distribution()
dist_config = dist.to_config(test_config.TestConfig())
paths = self.paths.replace_work('$W')
self.assertEqual('$W/scripts',
pipeline._create_pkgbuild_scripts(paths, dist_config))
manager.assert_has_calls([
mock.call.make_dir('$W/scripts'),
mock.call.write_file('$W/scripts/postinstall', mock.ANY),
mock.call.set_executable('$W/scripts/postinstall')
])
postinstall_string = manager.mock_calls[1][1][1]
self.assertEqual(
postinstall_string, """app dir is 'App Product.app'
app product is 'App Product'
framework dir is 'App Product.app/Contents/Frameworks/Product Framework.framework'"""
)
@mock.patch('signing.commands.plistlib.readPlist', _read_plist)
def test_productbuild_distribution_path(self, **kwargs):
manager = mock.Mock()
......@@ -316,6 +355,8 @@ class TestPipelineHelpers(unittest.TestCase):
@mock.patch('signing.pipeline._productbuild_distribution_path',
_productbuild_distribution_path)
@mock.patch('signing.pipeline._create_pkgbuild_scripts',
_create_pkgbuild_scripts)
def test_package_and_sign_pkg_no_branding(self, **kwargs):
manager = mock.Mock()
for attr in kwargs:
......@@ -344,6 +385,10 @@ class TestPipelineHelpers(unittest.TestCase):
_get_adjacent_item(pkgbuild_args, '--identifier'))
self.assertEqual('99.0.9999.99',
_get_adjacent_item(pkgbuild_args, '--version'))
self.assertEqual('$W/App Product.app',
_get_adjacent_item(pkgbuild_args, '--component'))
self.assertEqual('$W/scripts',
_get_adjacent_item(pkgbuild_args, '--scripts'))
self.assertEqual(
'$W/App Product.dist',
......@@ -391,6 +436,8 @@ class TestPipelineHelpers(unittest.TestCase):
@mock.patch('signing.pipeline._productbuild_distribution_path',
_productbuild_distribution_path)
@mock.patch('signing.pipeline._create_pkgbuild_scripts',
_create_pkgbuild_scripts)
def test_package_and_sign_pkg_branding(self, **kwargs):
manager = mock.Mock()
for attr in kwargs:
......@@ -423,6 +470,10 @@ class TestPipelineHelpers(unittest.TestCase):
_get_adjacent_item(pkgbuild_args, '--identifier'))
self.assertEqual('99.0.9999.99',
_get_adjacent_item(pkgbuild_args, '--version'))
self.assertEqual('$W/App Product.app',
_get_adjacent_item(pkgbuild_args, '--component'))
self.assertEqual('$W/scripts',
_get_adjacent_item(pkgbuild_args, '--scripts'))
self.assertEqual(
'$W/App Product.dist',
......
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