blob: 82552fc8e2c491f6498be02adf126efb28a50afd [file] [log] [blame]
Xiaochu Liudeed0232018-06-26 10:25:34 -07001# -*- coding: utf-8 -*-
2# Copyright 2018 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Script to generate a DLC (Downloadable Content) artifact."""
7
8from __future__ import print_function
9
10import hashlib
11import json
12import math
13import os
Amin Hassani11a88cf2019-01-29 15:31:24 -080014import shutil
Xiaochu Liudeed0232018-06-26 10:25:34 -070015
16from chromite.lib import commandline
17from chromite.lib import cros_build_lib
Amin Hassanib97a5ee2019-01-23 14:44:43 -080018from chromite.lib import cros_logging as logging
Xiaochu Liudeed0232018-06-26 10:25:34 -070019from chromite.lib import osutils
20
Amin Hassani8f1cc0f2019-03-06 15:34:53 -080021from chromite.scripts import cros_set_lsb_release
Xiaochu Liudeed0232018-06-26 10:25:34 -070022
Amin Hassani2af75a92019-01-22 21:07:45 -080023DLC_META_DIR = 'opt/google/dlc/'
24DLC_IMAGE_DIR = 'build/rootfs/dlc/'
Amin Hassanid5742d32019-01-22 21:13:34 -080025LSB_RELEASE = 'etc/lsb-release'
26
Amin Hassani11a88cf2019-01-29 15:31:24 -080027# This file has major and minor version numbers that the update_engine client
28# supports. These values are needed for generating a delta/full payload.
29UPDATE_ENGINE_CONF = 'etc/update_engine.conf'
30
31_EXTRA_RESOURCES = (
32 UPDATE_ENGINE_CONF,
33)
34
Amin Hassanid5742d32019-01-22 21:13:34 -080035DLC_ID_KEY = 'DLC_ID'
Amin Hassanib5a48042019-03-18 14:30:51 -070036DLC_PACKAGE_KEY = 'DLC_PACKAGE'
Amin Hassanid5742d32019-01-22 21:13:34 -080037DLC_NAME_KEY = 'DLC_NAME'
Amin Hassani8f1cc0f2019-03-06 15:34:53 -080038DLC_APPID_KEY = 'DLC_RELEASE_APPID'
Amin Hassani2af75a92019-01-22 21:07:45 -080039
Amin Hassani22a25eb2019-01-11 14:25:02 -080040_SQUASHFS_TYPE = 'squashfs'
41_EXT4_TYPE = 'ext4'
42
Amin Hassanid5742d32019-01-22 21:13:34 -080043
Xiaochu Liudeed0232018-06-26 10:25:34 -070044def HashFile(file_path):
45 """Calculate the sha256 hash of a file.
46
47 Args:
48 file_path: (str) path to the file.
49
50 Returns:
51 [str]: The sha256 hash of the file.
52 """
53 sha256 = hashlib.sha256()
54 with open(file_path, 'rb') as f:
55 for b in iter(lambda: f.read(2048), b''):
56 sha256.update(b)
57 return sha256.hexdigest()
58
59
Amin Hassani174eb7e2019-01-18 11:11:24 -080060class DlcGenerator(object):
Xiaochu Liudeed0232018-06-26 10:25:34 -070061 """Object to generate DLC artifacts."""
62 # Block size for the DLC image.
63 # We use 4K for various reasons:
64 # 1. it's what imageloader (linux kernel) supports.
65 # 2. it's what verity supports.
66 _BLOCK_SIZE = 4096
67 # Blocks in the initial sparse image.
68 _BLOCKS = 500000
69 # Version of manifest file.
70 _MANIFEST_VERSION = 1
71
Amin Hassanicc7ffce2019-01-11 14:57:52 -080072 # The DLC root path inside the DLC module.
73 _DLC_ROOT_DIR = 'root'
74
Amin Hassanib97a5ee2019-01-23 14:44:43 -080075 def __init__(self, src_dir, sysroot, install_root_dir, fs_type,
Amin Hassanib5a48042019-03-18 14:30:51 -070076 pre_allocated_blocks, version, dlc_id, dlc_package, name):
Xiaochu Liudeed0232018-06-26 10:25:34 -070077 """Object initializer.
78
79 Args:
Xiaochu Liudeed0232018-06-26 10:25:34 -070080 src_dir: (str) path to the DLC source root directory.
Amin Hassanib97a5ee2019-01-23 14:44:43 -080081 sysroot: (str) The path to the build root directory.
Amin Hassani2af75a92019-01-22 21:07:45 -080082 install_root_dir: (str) The path to the root installation directory.
Xiaochu Liudeed0232018-06-26 10:25:34 -070083 fs_type: (str) file system type.
84 pre_allocated_blocks: (int) number of blocks pre-allocated on device.
85 version: (str) DLC version.
86 dlc_id: (str) DLC ID.
Amin Hassanib5a48042019-03-18 14:30:51 -070087 dlc_package: (str) DLC Package.
Xiaochu Liudeed0232018-06-26 10:25:34 -070088 name: (str) DLC name.
89 """
90 self.src_dir = src_dir
Amin Hassanib97a5ee2019-01-23 14:44:43 -080091 self.sysroot = sysroot
Amin Hassani2af75a92019-01-22 21:07:45 -080092 self.install_root_dir = install_root_dir
Xiaochu Liudeed0232018-06-26 10:25:34 -070093 self.fs_type = fs_type
94 self.pre_allocated_blocks = pre_allocated_blocks
95 self.version = version
96 self.dlc_id = dlc_id
Amin Hassanib5a48042019-03-18 14:30:51 -070097 self.dlc_package = dlc_package
Xiaochu Liudeed0232018-06-26 10:25:34 -070098 self.name = name
Amin Hassani2af75a92019-01-22 21:07:45 -080099
100 self.meta_dir = os.path.join(self.install_root_dir, DLC_META_DIR,
Amin Hassanib5a48042019-03-18 14:30:51 -0700101 self.dlc_id, self.dlc_package)
Amin Hassani2af75a92019-01-22 21:07:45 -0800102 self.image_dir = os.path.join(self.install_root_dir, DLC_IMAGE_DIR,
Amin Hassanib5a48042019-03-18 14:30:51 -0700103 self.dlc_id, self.dlc_package)
Amin Hassani2af75a92019-01-22 21:07:45 -0800104 osutils.SafeMakedirs(self.meta_dir)
105 osutils.SafeMakedirs(self.image_dir)
106
Xiaochu Liudeed0232018-06-26 10:25:34 -0700107 # Create path for all final artifacts.
Amin Hassanib5a48042019-03-18 14:30:51 -0700108 self.dest_image = os.path.join(self.image_dir, 'dlc.img')
Amin Hassani2af75a92019-01-22 21:07:45 -0800109 self.dest_table = os.path.join(self.meta_dir, 'table')
110 self.dest_imageloader_json = os.path.join(self.meta_dir, 'imageloader.json')
Xiaochu Liudeed0232018-06-26 10:25:34 -0700111
112 def SquashOwnerships(self, path):
113 """Squash the owernships & permissions for files.
114
115 Args:
116 path: (str) path that contains all files to be processed.
117 """
118 cros_build_lib.SudoRunCommand(['chown', '-R', '0:0', path])
119 cros_build_lib.SudoRunCommand(
120 ['find', path, '-exec', 'touch', '-h', '-t', '197001010000.00', '{}',
121 '+'])
122
123 def CreateExt4Image(self):
124 """Create an ext4 image."""
125 with osutils.TempDir(prefix='dlc_') as temp_dir:
126 mount_point = os.path.join(temp_dir, 'mount_point')
127 # Create a raw image file.
128 with open(self.dest_image, 'w') as f:
129 f.truncate(self._BLOCKS * self._BLOCK_SIZE)
130 # Create an ext4 file system on the raw image.
131 cros_build_lib.RunCommand(
132 ['/sbin/mkfs.ext4', '-b', str(self._BLOCK_SIZE), '-O',
133 '^has_journal', self.dest_image], capture_output=True)
134 # Create the mount_point directory.
135 osutils.SafeMakedirs(mount_point)
136 # Mount the ext4 image.
137 osutils.MountDir(self.dest_image, mount_point, mount_opts=('loop', 'rw'))
Amin Hassanicc7ffce2019-01-11 14:57:52 -0800138
Xiaochu Liudeed0232018-06-26 10:25:34 -0700139 try:
Amin Hassani11a88cf2019-01-29 15:31:24 -0800140 self.SetupDlcImageFiles(mount_point)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700141 finally:
142 # Unmount the ext4 image.
143 osutils.UmountDir(mount_point)
144 # Shrink to minimum size.
145 cros_build_lib.RunCommand(
146 ['/sbin/e2fsck', '-y', '-f', self.dest_image], capture_output=True)
147 cros_build_lib.RunCommand(
148 ['/sbin/resize2fs', '-M', self.dest_image], capture_output=True)
149
150 def CreateSquashfsImage(self):
151 """Create a squashfs image."""
152 with osutils.TempDir(prefix='dlc_') as temp_dir:
Amin Hassani22a25eb2019-01-11 14:25:02 -0800153 squashfs_root = os.path.join(temp_dir, 'squashfs-root')
Amin Hassani11a88cf2019-01-29 15:31:24 -0800154 self.SetupDlcImageFiles(squashfs_root)
Amin Hassani22a25eb2019-01-11 14:25:02 -0800155
156 cros_build_lib.RunCommand(['mksquashfs', squashfs_root, self.dest_image,
157 '-4k-align', '-noappend'],
158 capture_output=True)
159
160 # We changed the ownership and permissions of the squashfs_root
161 # directory. Now we need to remove it manually.
162 osutils.RmDir(squashfs_root, sudo=True)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700163
Amin Hassani11a88cf2019-01-29 15:31:24 -0800164 def SetupDlcImageFiles(self, dlc_dir):
165 """Prepares the directory dlc_dir with all the files a DLC needs.
166
167 Args:
168 dlc_dir: (str) The path to where to setup files inside the DLC.
169 """
170 dlc_root_dir = os.path.join(dlc_dir, self._DLC_ROOT_DIR)
171 osutils.SafeMakedirs(dlc_root_dir)
172 osutils.CopyDirContents(self.src_dir, dlc_root_dir)
173 self.PrepareLsbRelease(dlc_dir)
174 self.CollectExtraResources(dlc_dir)
175 self.SquashOwnerships(dlc_dir)
176
Amin Hassanid5742d32019-01-22 21:13:34 -0800177 def PrepareLsbRelease(self, dlc_dir):
178 """Prepare the file /etc/lsb-release in the DLC module.
179
180 This file is used dropping some identification parameters for the DLC.
181
182 Args:
183 dlc_dir: (str) The path to root directory of the DLC. e.g. mounted point
184 when we are creating the image.
185 """
Amin Hassani8f1cc0f2019-03-06 15:34:53 -0800186 # Reading the platform APPID and creating the DLC APPID.
187 platform_lsb_release = osutils.ReadFile(os.path.join(self.sysroot,
188 LSB_RELEASE))
189 app_id = None
190 for line in platform_lsb_release.split('\n'):
191 if line.startswith(cros_set_lsb_release.LSB_KEY_APPID_RELEASE):
192 app_id = line.split('=')[1]
193 if app_id is None:
194 raise Exception('%s does not have a valid key %s' %
195 (platform_lsb_release,
196 cros_set_lsb_release.LSB_KEY_APPID_RELEASE))
Amin Hassanid5742d32019-01-22 21:13:34 -0800197
198 fields = {
199 DLC_ID_KEY: self.dlc_id,
Amin Hassanib5a48042019-03-18 14:30:51 -0700200 DLC_PACKAGE_KEY: self.dlc_package,
Amin Hassanid5742d32019-01-22 21:13:34 -0800201 DLC_NAME_KEY: self.name,
Amin Hassani8f1cc0f2019-03-06 15:34:53 -0800202 # The DLC appid is generated by concatenating the platform appid with
203 # the DLC ID using an underscore. This pattern should never be changed
204 # once set otherwise it can break a lot of things!
205 DLC_APPID_KEY: '%s_%s' % (app_id, self.dlc_id),
Amin Hassanid5742d32019-01-22 21:13:34 -0800206 }
Amin Hassani8f1cc0f2019-03-06 15:34:53 -0800207
208 lsb_release = os.path.join(dlc_dir, LSB_RELEASE)
209 osutils.SafeMakedirs(os.path.dirname(lsb_release))
Amin Hassanid5742d32019-01-22 21:13:34 -0800210 content = ''.join(['%s=%s\n' % (k, v) for k, v in fields.items()])
211 osutils.WriteFile(lsb_release, content)
212
Amin Hassani11a88cf2019-01-29 15:31:24 -0800213 def CollectExtraResources(self, dlc_dir):
214 """Collect the extra resources needed by the DLC module.
215
216 Look at the documentation around _EXTRA_RESOURCES.
217
218 Args:
219 dlc_dir: (str) The path to root directory of the DLC. e.g. mounted point
220 when we are creating the image.
221 """
222 for r in _EXTRA_RESOURCES:
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800223 source_path = os.path.join(self.sysroot, r)
Amin Hassani11a88cf2019-01-29 15:31:24 -0800224 target_path = os.path.join(dlc_dir, r)
225 osutils.SafeMakedirs(os.path.dirname(target_path))
226 shutil.copyfile(source_path, target_path)
227
Xiaochu Liudeed0232018-06-26 10:25:34 -0700228 def CreateImage(self):
229 """Create the image and copy the DLC files to it."""
Amin Hassani22a25eb2019-01-11 14:25:02 -0800230 if self.fs_type == _EXT4_TYPE:
Xiaochu Liudeed0232018-06-26 10:25:34 -0700231 self.CreateExt4Image()
Amin Hassani22a25eb2019-01-11 14:25:02 -0800232 elif self.fs_type == _SQUASHFS_TYPE:
Xiaochu Liudeed0232018-06-26 10:25:34 -0700233 self.CreateSquashfsImage()
234 else:
235 raise ValueError('Wrong fs type: %s used:' % self.fs_type)
236
237 def GetImageloaderJsonContent(self, image_hash, table_hash, blocks):
238 """Return the content of imageloader.json file.
239
240 Args:
241 image_hash: (str) sha256 hash of the DLC image.
242 table_hash: (str) sha256 hash of the DLC table file.
243 blocks: (int) number of blocks in the DLC image.
244
245 Returns:
246 [str]: content of imageloader.json file.
247 """
248 return {
249 'fs-type': self.fs_type,
250 'id': self.dlc_id,
Amin Hassanib5a48042019-03-18 14:30:51 -0700251 'package': self.dlc_package,
Xiaochu Liudeed0232018-06-26 10:25:34 -0700252 'image-sha256-hash': image_hash,
253 'image-type': 'dlc',
254 'is-removable': True,
255 'manifest-version': self._MANIFEST_VERSION,
256 'name': self.name,
257 'pre-allocated-size': self.pre_allocated_blocks * self._BLOCK_SIZE,
258 'size': blocks * self._BLOCK_SIZE,
259 'table-sha256-hash': table_hash,
260 'version': self.version,
261 }
262
263 def GenerateVerity(self):
264 """Generate verity parameters and hashes for the image."""
265 with osutils.TempDir(prefix='dlc_') as temp_dir:
266 hash_tree = os.path.join(temp_dir, 'hash_tree')
267 # Get blocks in the image.
268 blocks = math.ceil(
269 os.path.getsize(self.dest_image) / self._BLOCK_SIZE)
270 result = cros_build_lib.RunCommand(
271 ['verity', 'mode=create', 'alg=sha256', 'payload=' + self.dest_image,
272 'payload_blocks=' + str(blocks), 'hashtree=' + hash_tree,
273 'salt=random'], capture_output=True)
274 table = result.output
275
276 # Append the merkle tree to the image.
277 osutils.WriteFile(self.dest_image, osutils.ReadFile(hash_tree), 'a+')
278
279 # Write verity parameter to table file.
280 osutils.WriteFile(self.dest_table, table)
281
282 # Compute image hash.
283 image_hash = HashFile(self.dest_image)
284 table_hash = HashFile(self.dest_table)
285 # Write image hash to imageloader.json file.
286 blocks = math.ceil(
287 os.path.getsize(self.dest_image) / self._BLOCK_SIZE)
288 imageloader_json_content = self.GetImageloaderJsonContent(
289 image_hash, table_hash, int(blocks))
290 with open(self.dest_imageloader_json, 'w') as f:
291 json.dump(imageloader_json_content, f)
292
293 def GenerateDLC(self):
294 """Generate a DLC artifact."""
295 # Create the image and copy the DLC files to it.
296 self.CreateImage()
297 # Generate hash tree and other metadata.
298 self.GenerateVerity()
299
300
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800301def CopyAllDlcs(sysroot, install_root_dir):
302 """Copies all DLC image files into the images directory.
303
304 Copies the DLC image files in the given build directory into the given DLC
305 image directory. If the DLC build directory does not exist, or there is no DLC
306 for that board, this function does nothing.
307
308 Args:
309 sysroot: Path to directory containing DLC images, e.g /build/<board>.
310 install_root_dir: Path to DLC output directory,
311 e.g. src/build/images/<board>/<version>.
312 """
313 output_dir = os.path.join(install_root_dir, 'dlc')
314 build_dir = os.path.join(sysroot, DLC_IMAGE_DIR)
315
316 if not os.path.exists(build_dir) or not os.listdir(build_dir):
Amin Hassani6c0228b2019-03-04 13:42:33 -0800317 logging.info('There is no DLC to copy to output, ignoring.')
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800318 return
319
Amin Hassani6c0228b2019-03-04 13:42:33 -0800320 logging.info('Copying all DLC images to their destination path.')
321 logging.info('Detected the following DLCs: %s',
322 ', '.join(os.listdir(build_dir)))
323
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800324 osutils.SafeMakedirs(output_dir)
325 osutils.CopyDirContents(build_dir, output_dir)
326
Amin Hassani6c0228b2019-03-04 13:42:33 -0800327 logging.info('Done copying the DLCs to their destination.')
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800328
Xiaochu Liudeed0232018-06-26 10:25:34 -0700329def GetParser():
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800330 """Creates an argument parser and returns it."""
Xiaochu Liudeed0232018-06-26 10:25:34 -0700331 parser = commandline.ArgumentParser(description=__doc__)
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800332 # This script is used both for building an individual DLC or copying all final
333 # DLCs images to their final destination nearby chromiumsos_test_image.bin,
334 # etc. These two arguments are required in both cases.
335 parser.add_argument('--sysroot', type='path', metavar='DIR', required=True,
336 help="The root path to the board's build root, e.g. "
337 "/build/eve")
338 parser.add_argument('--install-root-dir', type='path', metavar='DIR',
339 required=True,
340 help='If building a specific DLC, it is the root path to'
341 ' install DLC images (%s) and metadata (%s). Otherwise it'
342 ' is the target directory where the Chrome OS images gets'
343 ' dropped in build_image, e.g. '
344 'src/build/images/<board>/latest.' % (DLC_IMAGE_DIR,
345 DLC_META_DIR))
Amin Hassani22a25eb2019-01-11 14:25:02 -0800346
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800347 one_dlc = parser.add_argument_group('Arguments required for building only '
348 'one DLC')
349 one_dlc.add_argument('--src-dir', type='path', metavar='SRC_DIR_PATH',
350 help='Root directory path that contains all DLC files '
351 'to be packed.')
352 one_dlc.add_argument('--pre-allocated-blocks', type=int,
353 metavar='PREALLOCATEDBLOCKS',
354 help='Number of blocks (block size is 4k) that need to'
355 'be pre-allocated on device.')
356 one_dlc.add_argument('--version', metavar='VERSION', help='DLC Version.')
357 one_dlc.add_argument('--id', metavar='ID', help='DLC ID (unique per DLC).')
Amin Hassanib5a48042019-03-18 14:30:51 -0700358 one_dlc.add_argument('--package', metavar='PACKAGE',
359 help='The package ID that is unique within a DLC, One'
360 ' DLC cannot have duplicate package IDs.')
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800361 one_dlc.add_argument('--name', metavar='NAME',
362 help='A human-readable name for the DLC.')
363 one_dlc.add_argument('--fs-type', metavar='FS_TYPE', default=_SQUASHFS_TYPE,
364 choices=(_SQUASHFS_TYPE, _EXT4_TYPE),
365 help='File system type of the image.')
Xiaochu Liudeed0232018-06-26 10:25:34 -0700366 return parser
367
368
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800369def ValidateArguments(opts):
370 """Validates the correctness of the passed arguments.
371
372 Args:
373 opts: Parsed arguments.
374 """
375 # Make sure if the intention is to build one DLC, all the required arguments
376 # are passed.
377 per_dlc_req_args = ('src_dir', 'pre_allocated_blocks', 'version', 'id',
Amin Hassanib5a48042019-03-18 14:30:51 -0700378 'package', 'name')
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800379 if (opts.id and
380 not all(vars(opts)[arg] is not None for arg in per_dlc_req_args)):
381 raise Exception('If the intention is to build only one DLC, all the flags'
382 '%s required for it should be passed .' % per_dlc_req_args)
383
384 if opts.fs_type == _EXT4_TYPE:
385 raise Exception('ext4 unsupported, see https://crbug.com/890060')
386
387
Xiaochu Liudeed0232018-06-26 10:25:34 -0700388def main(argv):
389 opts = GetParser().parse_args(argv)
390 opts.Freeze()
391
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800392 ValidateArguments(opts)
Amin Hassani2af75a92019-01-22 21:07:45 -0800393
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800394 if opts.id:
395 logging.info('Building DLC %s', opts.id)
396 dlc_generator = DlcGenerator(src_dir=opts.src_dir,
397 sysroot=opts.sysroot,
398 install_root_dir=opts.install_root_dir,
399 fs_type=opts.fs_type,
400 pre_allocated_blocks=opts.pre_allocated_blocks,
401 version=opts.version,
402 dlc_id=opts.id,
Amin Hassanib5a48042019-03-18 14:30:51 -0700403 dlc_package=opts.package,
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800404 name=opts.name)
405 dlc_generator.GenerateDLC()
406 else:
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800407 CopyAllDlcs(opts.sysroot, opts.install_root_dir)