blob: efb3c0b2247b2466fe7acdddc366c3368caa6406 [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
Mike Frysinger93e8ffa2019-07-03 20:24:18 -04008from __future__ import division
Xiaochu Liudeed0232018-06-26 10:25:34 -07009from __future__ import print_function
10
11import hashlib
12import json
13import math
14import os
Amin Hassani11a88cf2019-01-29 15:31:24 -080015import shutil
Xiaochu Liudeed0232018-06-26 10:25:34 -070016
17from chromite.lib import commandline
18from chromite.lib import cros_build_lib
Amin Hassanib97a5ee2019-01-23 14:44:43 -080019from chromite.lib import cros_logging as logging
Xiaochu Liudeed0232018-06-26 10:25:34 -070020from chromite.lib import osutils
21
Amin Hassani8f1cc0f2019-03-06 15:34:53 -080022from chromite.scripts import cros_set_lsb_release
Xiaochu Liudeed0232018-06-26 10:25:34 -070023
Amin Hassani2af75a92019-01-22 21:07:45 -080024DLC_META_DIR = 'opt/google/dlc/'
25DLC_IMAGE_DIR = 'build/rootfs/dlc/'
Amin Hassanid5742d32019-01-22 21:13:34 -080026LSB_RELEASE = 'etc/lsb-release'
27
Amin Hassani11a88cf2019-01-29 15:31:24 -080028# This file has major and minor version numbers that the update_engine client
29# supports. These values are needed for generating a delta/full payload.
30UPDATE_ENGINE_CONF = 'etc/update_engine.conf'
31
32_EXTRA_RESOURCES = (
33 UPDATE_ENGINE_CONF,
34)
35
Amin Hassanid5742d32019-01-22 21:13:34 -080036DLC_ID_KEY = 'DLC_ID'
Amin Hassanib5a48042019-03-18 14:30:51 -070037DLC_PACKAGE_KEY = 'DLC_PACKAGE'
Amin Hassanid5742d32019-01-22 21:13:34 -080038DLC_NAME_KEY = 'DLC_NAME'
Amin Hassani8f1cc0f2019-03-06 15:34:53 -080039DLC_APPID_KEY = 'DLC_RELEASE_APPID'
Amin Hassani2af75a92019-01-22 21:07:45 -080040
Amin Hassani22a25eb2019-01-11 14:25:02 -080041_SQUASHFS_TYPE = 'squashfs'
42_EXT4_TYPE = 'ext4'
43
Amin Hassanid5742d32019-01-22 21:13:34 -080044
Xiaochu Liudeed0232018-06-26 10:25:34 -070045def HashFile(file_path):
46 """Calculate the sha256 hash of a file.
47
48 Args:
49 file_path: (str) path to the file.
50
51 Returns:
52 [str]: The sha256 hash of the file.
53 """
54 sha256 = hashlib.sha256()
55 with open(file_path, 'rb') as f:
56 for b in iter(lambda: f.read(2048), b''):
57 sha256.update(b)
58 return sha256.hexdigest()
59
60
Amin Hassani174eb7e2019-01-18 11:11:24 -080061class DlcGenerator(object):
Xiaochu Liudeed0232018-06-26 10:25:34 -070062 """Object to generate DLC artifacts."""
63 # Block size for the DLC image.
64 # We use 4K for various reasons:
65 # 1. it's what imageloader (linux kernel) supports.
66 # 2. it's what verity supports.
67 _BLOCK_SIZE = 4096
68 # Blocks in the initial sparse image.
69 _BLOCKS = 500000
70 # Version of manifest file.
71 _MANIFEST_VERSION = 1
72
Amin Hassanicc7ffce2019-01-11 14:57:52 -080073 # The DLC root path inside the DLC module.
74 _DLC_ROOT_DIR = 'root'
75
Amin Hassanib97a5ee2019-01-23 14:44:43 -080076 def __init__(self, src_dir, sysroot, install_root_dir, fs_type,
Amin Hassanib5a48042019-03-18 14:30:51 -070077 pre_allocated_blocks, version, dlc_id, dlc_package, name):
Xiaochu Liudeed0232018-06-26 10:25:34 -070078 """Object initializer.
79
80 Args:
Xiaochu Liudeed0232018-06-26 10:25:34 -070081 src_dir: (str) path to the DLC source root directory.
Amin Hassanib97a5ee2019-01-23 14:44:43 -080082 sysroot: (str) The path to the build root directory.
Amin Hassani2af75a92019-01-22 21:07:45 -080083 install_root_dir: (str) The path to the root installation directory.
Xiaochu Liudeed0232018-06-26 10:25:34 -070084 fs_type: (str) file system type.
85 pre_allocated_blocks: (int) number of blocks pre-allocated on device.
86 version: (str) DLC version.
87 dlc_id: (str) DLC ID.
Amin Hassanib5a48042019-03-18 14:30:51 -070088 dlc_package: (str) DLC Package.
Xiaochu Liudeed0232018-06-26 10:25:34 -070089 name: (str) DLC name.
90 """
91 self.src_dir = src_dir
Amin Hassanib97a5ee2019-01-23 14:44:43 -080092 self.sysroot = sysroot
Amin Hassani2af75a92019-01-22 21:07:45 -080093 self.install_root_dir = install_root_dir
Xiaochu Liudeed0232018-06-26 10:25:34 -070094 self.fs_type = fs_type
95 self.pre_allocated_blocks = pre_allocated_blocks
96 self.version = version
97 self.dlc_id = dlc_id
Amin Hassanib5a48042019-03-18 14:30:51 -070098 self.dlc_package = dlc_package
Xiaochu Liudeed0232018-06-26 10:25:34 -070099 self.name = name
Amin Hassani2af75a92019-01-22 21:07:45 -0800100
101 self.meta_dir = os.path.join(self.install_root_dir, DLC_META_DIR,
Amin Hassanib5a48042019-03-18 14:30:51 -0700102 self.dlc_id, self.dlc_package)
Amin Hassani2af75a92019-01-22 21:07:45 -0800103 self.image_dir = os.path.join(self.install_root_dir, DLC_IMAGE_DIR,
Amin Hassanib5a48042019-03-18 14:30:51 -0700104 self.dlc_id, self.dlc_package)
Amin Hassani2af75a92019-01-22 21:07:45 -0800105 osutils.SafeMakedirs(self.meta_dir)
106 osutils.SafeMakedirs(self.image_dir)
107
Xiaochu Liudeed0232018-06-26 10:25:34 -0700108 # Create path for all final artifacts.
Amin Hassanib5a48042019-03-18 14:30:51 -0700109 self.dest_image = os.path.join(self.image_dir, 'dlc.img')
Amin Hassani2af75a92019-01-22 21:07:45 -0800110 self.dest_table = os.path.join(self.meta_dir, 'table')
111 self.dest_imageloader_json = os.path.join(self.meta_dir, 'imageloader.json')
Xiaochu Liudeed0232018-06-26 10:25:34 -0700112
113 def SquashOwnerships(self, path):
114 """Squash the owernships & permissions for files.
115
116 Args:
117 path: (str) path that contains all files to be processed.
118 """
119 cros_build_lib.SudoRunCommand(['chown', '-R', '0:0', path])
120 cros_build_lib.SudoRunCommand(
121 ['find', path, '-exec', 'touch', '-h', '-t', '197001010000.00', '{}',
122 '+'])
123
124 def CreateExt4Image(self):
125 """Create an ext4 image."""
126 with osutils.TempDir(prefix='dlc_') as temp_dir:
127 mount_point = os.path.join(temp_dir, 'mount_point')
128 # Create a raw image file.
129 with open(self.dest_image, 'w') as f:
130 f.truncate(self._BLOCKS * self._BLOCK_SIZE)
131 # Create an ext4 file system on the raw image.
132 cros_build_lib.RunCommand(
133 ['/sbin/mkfs.ext4', '-b', str(self._BLOCK_SIZE), '-O',
134 '^has_journal', self.dest_image], capture_output=True)
135 # Create the mount_point directory.
136 osutils.SafeMakedirs(mount_point)
137 # Mount the ext4 image.
138 osutils.MountDir(self.dest_image, mount_point, mount_opts=('loop', 'rw'))
Amin Hassanicc7ffce2019-01-11 14:57:52 -0800139
Xiaochu Liudeed0232018-06-26 10:25:34 -0700140 try:
Amin Hassani11a88cf2019-01-29 15:31:24 -0800141 self.SetupDlcImageFiles(mount_point)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700142 finally:
143 # Unmount the ext4 image.
144 osutils.UmountDir(mount_point)
145 # Shrink to minimum size.
146 cros_build_lib.RunCommand(
147 ['/sbin/e2fsck', '-y', '-f', self.dest_image], capture_output=True)
148 cros_build_lib.RunCommand(
149 ['/sbin/resize2fs', '-M', self.dest_image], capture_output=True)
150
151 def CreateSquashfsImage(self):
152 """Create a squashfs image."""
153 with osutils.TempDir(prefix='dlc_') as temp_dir:
Amin Hassani22a25eb2019-01-11 14:25:02 -0800154 squashfs_root = os.path.join(temp_dir, 'squashfs-root')
Amin Hassani11a88cf2019-01-29 15:31:24 -0800155 self.SetupDlcImageFiles(squashfs_root)
Amin Hassani22a25eb2019-01-11 14:25:02 -0800156
157 cros_build_lib.RunCommand(['mksquashfs', squashfs_root, self.dest_image,
158 '-4k-align', '-noappend'],
159 capture_output=True)
160
161 # We changed the ownership and permissions of the squashfs_root
162 # directory. Now we need to remove it manually.
163 osutils.RmDir(squashfs_root, sudo=True)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700164
Amin Hassani11a88cf2019-01-29 15:31:24 -0800165 def SetupDlcImageFiles(self, dlc_dir):
166 """Prepares the directory dlc_dir with all the files a DLC needs.
167
168 Args:
169 dlc_dir: (str) The path to where to setup files inside the DLC.
170 """
171 dlc_root_dir = os.path.join(dlc_dir, self._DLC_ROOT_DIR)
172 osutils.SafeMakedirs(dlc_root_dir)
173 osutils.CopyDirContents(self.src_dir, dlc_root_dir)
174 self.PrepareLsbRelease(dlc_dir)
175 self.CollectExtraResources(dlc_dir)
176 self.SquashOwnerships(dlc_dir)
177
Amin Hassanid5742d32019-01-22 21:13:34 -0800178 def PrepareLsbRelease(self, dlc_dir):
179 """Prepare the file /etc/lsb-release in the DLC module.
180
181 This file is used dropping some identification parameters for the DLC.
182
183 Args:
184 dlc_dir: (str) The path to root directory of the DLC. e.g. mounted point
185 when we are creating the image.
186 """
Amin Hassani8f1cc0f2019-03-06 15:34:53 -0800187 # Reading the platform APPID and creating the DLC APPID.
188 platform_lsb_release = osutils.ReadFile(os.path.join(self.sysroot,
189 LSB_RELEASE))
190 app_id = None
191 for line in platform_lsb_release.split('\n'):
192 if line.startswith(cros_set_lsb_release.LSB_KEY_APPID_RELEASE):
193 app_id = line.split('=')[1]
194 if app_id is None:
195 raise Exception('%s does not have a valid key %s' %
196 (platform_lsb_release,
197 cros_set_lsb_release.LSB_KEY_APPID_RELEASE))
Amin Hassanid5742d32019-01-22 21:13:34 -0800198
199 fields = {
200 DLC_ID_KEY: self.dlc_id,
Amin Hassanib5a48042019-03-18 14:30:51 -0700201 DLC_PACKAGE_KEY: self.dlc_package,
Amin Hassanid5742d32019-01-22 21:13:34 -0800202 DLC_NAME_KEY: self.name,
Amin Hassani8f1cc0f2019-03-06 15:34:53 -0800203 # The DLC appid is generated by concatenating the platform appid with
204 # the DLC ID using an underscore. This pattern should never be changed
205 # once set otherwise it can break a lot of things!
206 DLC_APPID_KEY: '%s_%s' % (app_id, self.dlc_id),
Amin Hassanid5742d32019-01-22 21:13:34 -0800207 }
Amin Hassani8f1cc0f2019-03-06 15:34:53 -0800208
209 lsb_release = os.path.join(dlc_dir, LSB_RELEASE)
210 osutils.SafeMakedirs(os.path.dirname(lsb_release))
Amin Hassanid5742d32019-01-22 21:13:34 -0800211 content = ''.join(['%s=%s\n' % (k, v) for k, v in fields.items()])
212 osutils.WriteFile(lsb_release, content)
213
Amin Hassani11a88cf2019-01-29 15:31:24 -0800214 def CollectExtraResources(self, dlc_dir):
215 """Collect the extra resources needed by the DLC module.
216
217 Look at the documentation around _EXTRA_RESOURCES.
218
219 Args:
220 dlc_dir: (str) The path to root directory of the DLC. e.g. mounted point
221 when we are creating the image.
222 """
223 for r in _EXTRA_RESOURCES:
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800224 source_path = os.path.join(self.sysroot, r)
Amin Hassani11a88cf2019-01-29 15:31:24 -0800225 target_path = os.path.join(dlc_dir, r)
226 osutils.SafeMakedirs(os.path.dirname(target_path))
227 shutil.copyfile(source_path, target_path)
228
Xiaochu Liudeed0232018-06-26 10:25:34 -0700229 def CreateImage(self):
230 """Create the image and copy the DLC files to it."""
Amin Hassani22a25eb2019-01-11 14:25:02 -0800231 if self.fs_type == _EXT4_TYPE:
Xiaochu Liudeed0232018-06-26 10:25:34 -0700232 self.CreateExt4Image()
Amin Hassani22a25eb2019-01-11 14:25:02 -0800233 elif self.fs_type == _SQUASHFS_TYPE:
Xiaochu Liudeed0232018-06-26 10:25:34 -0700234 self.CreateSquashfsImage()
235 else:
236 raise ValueError('Wrong fs type: %s used:' % self.fs_type)
237
Xiaochu Liu36b30592019-08-06 09:39:54 -0700238 self.VerifyImageSize(os.path.getsize(self.dest_image))
239
240 def VerifyImageSize(self, image_bytes):
241 """Verify the image can fit to the reserved file."""
242 preallocated_bytes = self.pre_allocated_blocks * self._BLOCK_SIZE
243 # Verifies the actual size of the DLC image is NOT smaller than the
244 # preallocated space.
245 if preallocated_bytes < image_bytes:
246 raise ValueError(
247 'The DLC_PREALLOC_BLOCKS (%s) value set in DLC ebuild resulted in a '
248 'max size of DLC_PREALLOC_BLOCKS * 4K (%s) bytes the DLC image is '
249 'allowed to occupy. The value is smaller than the actual image size '
250 '(%s) required. Increase DLC_PREALLOC_BLOCKS in your ebuild to at '
251 'least %d.' % (
252 self.pre_allocated_blocks, preallocated_bytes, image_bytes,
253 image_bytes // self._BLOCK_SIZE))
254
Xiaochu Liudeed0232018-06-26 10:25:34 -0700255 def GetImageloaderJsonContent(self, image_hash, table_hash, blocks):
256 """Return the content of imageloader.json file.
257
258 Args:
259 image_hash: (str) sha256 hash of the DLC image.
260 table_hash: (str) sha256 hash of the DLC table file.
261 blocks: (int) number of blocks in the DLC image.
262
263 Returns:
264 [str]: content of imageloader.json file.
265 """
266 return {
267 'fs-type': self.fs_type,
268 'id': self.dlc_id,
Amin Hassanib5a48042019-03-18 14:30:51 -0700269 'package': self.dlc_package,
Xiaochu Liudeed0232018-06-26 10:25:34 -0700270 'image-sha256-hash': image_hash,
271 'image-type': 'dlc',
272 'is-removable': True,
273 'manifest-version': self._MANIFEST_VERSION,
274 'name': self.name,
275 'pre-allocated-size': self.pre_allocated_blocks * self._BLOCK_SIZE,
276 'size': blocks * self._BLOCK_SIZE,
277 'table-sha256-hash': table_hash,
278 'version': self.version,
279 }
280
281 def GenerateVerity(self):
282 """Generate verity parameters and hashes for the image."""
283 with osutils.TempDir(prefix='dlc_') as temp_dir:
284 hash_tree = os.path.join(temp_dir, 'hash_tree')
285 # Get blocks in the image.
286 blocks = math.ceil(
287 os.path.getsize(self.dest_image) / self._BLOCK_SIZE)
288 result = cros_build_lib.RunCommand(
289 ['verity', 'mode=create', 'alg=sha256', 'payload=' + self.dest_image,
290 'payload_blocks=' + str(blocks), 'hashtree=' + hash_tree,
291 'salt=random'], capture_output=True)
292 table = result.output
293
294 # Append the merkle tree to the image.
295 osutils.WriteFile(self.dest_image, osutils.ReadFile(hash_tree), 'a+')
296
297 # Write verity parameter to table file.
298 osutils.WriteFile(self.dest_table, table)
299
300 # Compute image hash.
301 image_hash = HashFile(self.dest_image)
302 table_hash = HashFile(self.dest_table)
303 # Write image hash to imageloader.json file.
304 blocks = math.ceil(
305 os.path.getsize(self.dest_image) / self._BLOCK_SIZE)
306 imageloader_json_content = self.GetImageloaderJsonContent(
307 image_hash, table_hash, int(blocks))
308 with open(self.dest_imageloader_json, 'w') as f:
309 json.dump(imageloader_json_content, f)
310
311 def GenerateDLC(self):
312 """Generate a DLC artifact."""
313 # Create the image and copy the DLC files to it.
314 self.CreateImage()
315 # Generate hash tree and other metadata.
316 self.GenerateVerity()
317
318
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800319def CopyAllDlcs(sysroot, install_root_dir):
320 """Copies all DLC image files into the images directory.
321
322 Copies the DLC image files in the given build directory into the given DLC
323 image directory. If the DLC build directory does not exist, or there is no DLC
324 for that board, this function does nothing.
325
326 Args:
327 sysroot: Path to directory containing DLC images, e.g /build/<board>.
328 install_root_dir: Path to DLC output directory,
329 e.g. src/build/images/<board>/<version>.
330 """
331 output_dir = os.path.join(install_root_dir, 'dlc')
332 build_dir = os.path.join(sysroot, DLC_IMAGE_DIR)
333
334 if not os.path.exists(build_dir) or not os.listdir(build_dir):
Amin Hassani6c0228b2019-03-04 13:42:33 -0800335 logging.info('There is no DLC to copy to output, ignoring.')
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800336 return
337
Amin Hassani6c0228b2019-03-04 13:42:33 -0800338 logging.info('Copying all DLC images to their destination path.')
339 logging.info('Detected the following DLCs: %s',
340 ', '.join(os.listdir(build_dir)))
341
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800342 osutils.SafeMakedirs(output_dir)
343 osutils.CopyDirContents(build_dir, output_dir)
344
Amin Hassani6c0228b2019-03-04 13:42:33 -0800345 logging.info('Done copying the DLCs to their destination.')
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800346
Xiaochu Liudeed0232018-06-26 10:25:34 -0700347def GetParser():
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800348 """Creates an argument parser and returns it."""
Xiaochu Liudeed0232018-06-26 10:25:34 -0700349 parser = commandline.ArgumentParser(description=__doc__)
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800350 # This script is used both for building an individual DLC or copying all final
351 # DLCs images to their final destination nearby chromiumsos_test_image.bin,
352 # etc. These two arguments are required in both cases.
353 parser.add_argument('--sysroot', type='path', metavar='DIR', required=True,
354 help="The root path to the board's build root, e.g. "
Mike Frysinger80de5012019-08-01 14:10:53 -0400355 '/build/eve')
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800356 parser.add_argument('--install-root-dir', type='path', metavar='DIR',
357 required=True,
358 help='If building a specific DLC, it is the root path to'
359 ' install DLC images (%s) and metadata (%s). Otherwise it'
360 ' is the target directory where the Chrome OS images gets'
361 ' dropped in build_image, e.g. '
362 'src/build/images/<board>/latest.' % (DLC_IMAGE_DIR,
363 DLC_META_DIR))
Amin Hassani22a25eb2019-01-11 14:25:02 -0800364
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800365 one_dlc = parser.add_argument_group('Arguments required for building only '
366 'one DLC')
367 one_dlc.add_argument('--src-dir', type='path', metavar='SRC_DIR_PATH',
368 help='Root directory path that contains all DLC files '
369 'to be packed.')
370 one_dlc.add_argument('--pre-allocated-blocks', type=int,
371 metavar='PREALLOCATEDBLOCKS',
372 help='Number of blocks (block size is 4k) that need to'
373 'be pre-allocated on device.')
374 one_dlc.add_argument('--version', metavar='VERSION', help='DLC Version.')
375 one_dlc.add_argument('--id', metavar='ID', help='DLC ID (unique per DLC).')
Amin Hassanib5a48042019-03-18 14:30:51 -0700376 one_dlc.add_argument('--package', metavar='PACKAGE',
377 help='The package ID that is unique within a DLC, One'
378 ' DLC cannot have duplicate package IDs.')
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800379 one_dlc.add_argument('--name', metavar='NAME',
380 help='A human-readable name for the DLC.')
381 one_dlc.add_argument('--fs-type', metavar='FS_TYPE', default=_SQUASHFS_TYPE,
382 choices=(_SQUASHFS_TYPE, _EXT4_TYPE),
383 help='File system type of the image.')
Xiaochu Liudeed0232018-06-26 10:25:34 -0700384 return parser
385
386
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800387def ValidateArguments(opts):
388 """Validates the correctness of the passed arguments.
389
390 Args:
391 opts: Parsed arguments.
392 """
393 # Make sure if the intention is to build one DLC, all the required arguments
394 # are passed.
395 per_dlc_req_args = ('src_dir', 'pre_allocated_blocks', 'version', 'id',
Amin Hassanib5a48042019-03-18 14:30:51 -0700396 'package', 'name')
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800397 if (opts.id and
398 not all(vars(opts)[arg] is not None for arg in per_dlc_req_args)):
399 raise Exception('If the intention is to build only one DLC, all the flags'
400 '%s required for it should be passed .' % per_dlc_req_args)
401
402 if opts.fs_type == _EXT4_TYPE:
403 raise Exception('ext4 unsupported, see https://crbug.com/890060')
404
405
Xiaochu Liudeed0232018-06-26 10:25:34 -0700406def main(argv):
407 opts = GetParser().parse_args(argv)
408 opts.Freeze()
409
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800410 ValidateArguments(opts)
Amin Hassani2af75a92019-01-22 21:07:45 -0800411
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800412 if opts.id:
413 logging.info('Building DLC %s', opts.id)
414 dlc_generator = DlcGenerator(src_dir=opts.src_dir,
415 sysroot=opts.sysroot,
416 install_root_dir=opts.install_root_dir,
417 fs_type=opts.fs_type,
418 pre_allocated_blocks=opts.pre_allocated_blocks,
419 version=opts.version,
420 dlc_id=opts.id,
Amin Hassanib5a48042019-03-18 14:30:51 -0700421 dlc_package=opts.package,
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800422 name=opts.name)
423 dlc_generator.GenerateDLC()
424 else:
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800425 CopyAllDlcs(opts.sysroot, opts.install_root_dir)