blob: 3783a1def6f657821f731576fe3f5036cc190230 [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'
36DLC_NAME_KEY = 'DLC_NAME'
Amin Hassani8f1cc0f2019-03-06 15:34:53 -080037DLC_APPID_KEY = 'DLC_RELEASE_APPID'
Amin Hassani2af75a92019-01-22 21:07:45 -080038
Amin Hassani22a25eb2019-01-11 14:25:02 -080039_SQUASHFS_TYPE = 'squashfs'
40_EXT4_TYPE = 'ext4'
41
Amin Hassanid5742d32019-01-22 21:13:34 -080042
Xiaochu Liudeed0232018-06-26 10:25:34 -070043def HashFile(file_path):
44 """Calculate the sha256 hash of a file.
45
46 Args:
47 file_path: (str) path to the file.
48
49 Returns:
50 [str]: The sha256 hash of the file.
51 """
52 sha256 = hashlib.sha256()
53 with open(file_path, 'rb') as f:
54 for b in iter(lambda: f.read(2048), b''):
55 sha256.update(b)
56 return sha256.hexdigest()
57
58
Amin Hassani174eb7e2019-01-18 11:11:24 -080059class DlcGenerator(object):
Xiaochu Liudeed0232018-06-26 10:25:34 -070060 """Object to generate DLC artifacts."""
61 # Block size for the DLC image.
62 # We use 4K for various reasons:
63 # 1. it's what imageloader (linux kernel) supports.
64 # 2. it's what verity supports.
65 _BLOCK_SIZE = 4096
66 # Blocks in the initial sparse image.
67 _BLOCKS = 500000
68 # Version of manifest file.
69 _MANIFEST_VERSION = 1
70
Amin Hassanicc7ffce2019-01-11 14:57:52 -080071 # The DLC root path inside the DLC module.
72 _DLC_ROOT_DIR = 'root'
73
Amin Hassanib97a5ee2019-01-23 14:44:43 -080074 def __init__(self, src_dir, sysroot, install_root_dir, fs_type,
Amin Hassani11a88cf2019-01-29 15:31:24 -080075 pre_allocated_blocks, version, dlc_id, name):
Xiaochu Liudeed0232018-06-26 10:25:34 -070076 """Object initializer.
77
78 Args:
Xiaochu Liudeed0232018-06-26 10:25:34 -070079 src_dir: (str) path to the DLC source root directory.
Amin Hassanib97a5ee2019-01-23 14:44:43 -080080 sysroot: (str) The path to the build root directory.
Amin Hassani2af75a92019-01-22 21:07:45 -080081 install_root_dir: (str) The path to the root installation directory.
Xiaochu Liudeed0232018-06-26 10:25:34 -070082 fs_type: (str) file system type.
83 pre_allocated_blocks: (int) number of blocks pre-allocated on device.
84 version: (str) DLC version.
85 dlc_id: (str) DLC ID.
86 name: (str) DLC name.
87 """
88 self.src_dir = src_dir
Amin Hassanib97a5ee2019-01-23 14:44:43 -080089 self.sysroot = sysroot
Amin Hassani2af75a92019-01-22 21:07:45 -080090 self.install_root_dir = install_root_dir
Xiaochu Liudeed0232018-06-26 10:25:34 -070091 self.fs_type = fs_type
92 self.pre_allocated_blocks = pre_allocated_blocks
93 self.version = version
94 self.dlc_id = dlc_id
95 self.name = name
Amin Hassani2af75a92019-01-22 21:07:45 -080096
97 self.meta_dir = os.path.join(self.install_root_dir, DLC_META_DIR,
98 self.dlc_id)
99 self.image_dir = os.path.join(self.install_root_dir, DLC_IMAGE_DIR,
100 self.dlc_id)
101 osutils.SafeMakedirs(self.meta_dir)
102 osutils.SafeMakedirs(self.image_dir)
103
Xiaochu Liudeed0232018-06-26 10:25:34 -0700104 # Create path for all final artifacts.
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800105 self.dest_image = os.path.join(self.image_dir, self.GetImageFileName())
Amin Hassani2af75a92019-01-22 21:07:45 -0800106 self.dest_table = os.path.join(self.meta_dir, 'table')
107 self.dest_imageloader_json = os.path.join(self.meta_dir, 'imageloader.json')
Xiaochu Liudeed0232018-06-26 10:25:34 -0700108
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800109 def GetImageFileName(self):
110 """Returns the image file name created based on the dlc_id.
111
112 This probably will be replaced by the partition name once we move to a
113 multip-partition DLC.
114
115 Returns:
116 [str]: The image file name for the DLC.
117 """
118 return 'dlc_%s.img' % self.dlc_id
119
Xiaochu Liudeed0232018-06-26 10:25:34 -0700120 def SquashOwnerships(self, path):
121 """Squash the owernships & permissions for files.
122
123 Args:
124 path: (str) path that contains all files to be processed.
125 """
126 cros_build_lib.SudoRunCommand(['chown', '-R', '0:0', path])
127 cros_build_lib.SudoRunCommand(
128 ['find', path, '-exec', 'touch', '-h', '-t', '197001010000.00', '{}',
129 '+'])
130
131 def CreateExt4Image(self):
132 """Create an ext4 image."""
133 with osutils.TempDir(prefix='dlc_') as temp_dir:
134 mount_point = os.path.join(temp_dir, 'mount_point')
135 # Create a raw image file.
136 with open(self.dest_image, 'w') as f:
137 f.truncate(self._BLOCKS * self._BLOCK_SIZE)
138 # Create an ext4 file system on the raw image.
139 cros_build_lib.RunCommand(
140 ['/sbin/mkfs.ext4', '-b', str(self._BLOCK_SIZE), '-O',
141 '^has_journal', self.dest_image], capture_output=True)
142 # Create the mount_point directory.
143 osutils.SafeMakedirs(mount_point)
144 # Mount the ext4 image.
145 osutils.MountDir(self.dest_image, mount_point, mount_opts=('loop', 'rw'))
Amin Hassanicc7ffce2019-01-11 14:57:52 -0800146
Xiaochu Liudeed0232018-06-26 10:25:34 -0700147 try:
Amin Hassani11a88cf2019-01-29 15:31:24 -0800148 self.SetupDlcImageFiles(mount_point)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700149 finally:
150 # Unmount the ext4 image.
151 osutils.UmountDir(mount_point)
152 # Shrink to minimum size.
153 cros_build_lib.RunCommand(
154 ['/sbin/e2fsck', '-y', '-f', self.dest_image], capture_output=True)
155 cros_build_lib.RunCommand(
156 ['/sbin/resize2fs', '-M', self.dest_image], capture_output=True)
157
158 def CreateSquashfsImage(self):
159 """Create a squashfs image."""
160 with osutils.TempDir(prefix='dlc_') as temp_dir:
Amin Hassani22a25eb2019-01-11 14:25:02 -0800161 squashfs_root = os.path.join(temp_dir, 'squashfs-root')
Amin Hassani11a88cf2019-01-29 15:31:24 -0800162 self.SetupDlcImageFiles(squashfs_root)
Amin Hassani22a25eb2019-01-11 14:25:02 -0800163
164 cros_build_lib.RunCommand(['mksquashfs', squashfs_root, self.dest_image,
165 '-4k-align', '-noappend'],
166 capture_output=True)
167
168 # We changed the ownership and permissions of the squashfs_root
169 # directory. Now we need to remove it manually.
170 osutils.RmDir(squashfs_root, sudo=True)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700171
Amin Hassani11a88cf2019-01-29 15:31:24 -0800172 def SetupDlcImageFiles(self, dlc_dir):
173 """Prepares the directory dlc_dir with all the files a DLC needs.
174
175 Args:
176 dlc_dir: (str) The path to where to setup files inside the DLC.
177 """
178 dlc_root_dir = os.path.join(dlc_dir, self._DLC_ROOT_DIR)
179 osutils.SafeMakedirs(dlc_root_dir)
180 osutils.CopyDirContents(self.src_dir, dlc_root_dir)
181 self.PrepareLsbRelease(dlc_dir)
182 self.CollectExtraResources(dlc_dir)
183 self.SquashOwnerships(dlc_dir)
184
Amin Hassanid5742d32019-01-22 21:13:34 -0800185 def PrepareLsbRelease(self, dlc_dir):
186 """Prepare the file /etc/lsb-release in the DLC module.
187
188 This file is used dropping some identification parameters for the DLC.
189
190 Args:
191 dlc_dir: (str) The path to root directory of the DLC. e.g. mounted point
192 when we are creating the image.
193 """
Amin Hassani8f1cc0f2019-03-06 15:34:53 -0800194 # Reading the platform APPID and creating the DLC APPID.
195 platform_lsb_release = osutils.ReadFile(os.path.join(self.sysroot,
196 LSB_RELEASE))
197 app_id = None
198 for line in platform_lsb_release.split('\n'):
199 if line.startswith(cros_set_lsb_release.LSB_KEY_APPID_RELEASE):
200 app_id = line.split('=')[1]
201 if app_id is None:
202 raise Exception('%s does not have a valid key %s' %
203 (platform_lsb_release,
204 cros_set_lsb_release.LSB_KEY_APPID_RELEASE))
Amin Hassanid5742d32019-01-22 21:13:34 -0800205
206 fields = {
207 DLC_ID_KEY: self.dlc_id,
208 DLC_NAME_KEY: self.name,
Amin Hassani8f1cc0f2019-03-06 15:34:53 -0800209 # The DLC appid is generated by concatenating the platform appid with
210 # the DLC ID using an underscore. This pattern should never be changed
211 # once set otherwise it can break a lot of things!
212 DLC_APPID_KEY: '%s_%s' % (app_id, self.dlc_id),
Amin Hassanid5742d32019-01-22 21:13:34 -0800213 }
Amin Hassani8f1cc0f2019-03-06 15:34:53 -0800214
215 lsb_release = os.path.join(dlc_dir, LSB_RELEASE)
216 osutils.SafeMakedirs(os.path.dirname(lsb_release))
Amin Hassanid5742d32019-01-22 21:13:34 -0800217 content = ''.join(['%s=%s\n' % (k, v) for k, v in fields.items()])
218 osutils.WriteFile(lsb_release, content)
219
Amin Hassani11a88cf2019-01-29 15:31:24 -0800220 def CollectExtraResources(self, dlc_dir):
221 """Collect the extra resources needed by the DLC module.
222
223 Look at the documentation around _EXTRA_RESOURCES.
224
225 Args:
226 dlc_dir: (str) The path to root directory of the DLC. e.g. mounted point
227 when we are creating the image.
228 """
229 for r in _EXTRA_RESOURCES:
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800230 source_path = os.path.join(self.sysroot, r)
Amin Hassani11a88cf2019-01-29 15:31:24 -0800231 target_path = os.path.join(dlc_dir, r)
232 osutils.SafeMakedirs(os.path.dirname(target_path))
233 shutil.copyfile(source_path, target_path)
234
Xiaochu Liudeed0232018-06-26 10:25:34 -0700235 def CreateImage(self):
236 """Create the image and copy the DLC files to it."""
Amin Hassani22a25eb2019-01-11 14:25:02 -0800237 if self.fs_type == _EXT4_TYPE:
Xiaochu Liudeed0232018-06-26 10:25:34 -0700238 self.CreateExt4Image()
Amin Hassani22a25eb2019-01-11 14:25:02 -0800239 elif self.fs_type == _SQUASHFS_TYPE:
Xiaochu Liudeed0232018-06-26 10:25:34 -0700240 self.CreateSquashfsImage()
241 else:
242 raise ValueError('Wrong fs type: %s used:' % self.fs_type)
243
244 def GetImageloaderJsonContent(self, image_hash, table_hash, blocks):
245 """Return the content of imageloader.json file.
246
247 Args:
248 image_hash: (str) sha256 hash of the DLC image.
249 table_hash: (str) sha256 hash of the DLC table file.
250 blocks: (int) number of blocks in the DLC image.
251
252 Returns:
253 [str]: content of imageloader.json file.
254 """
255 return {
256 'fs-type': self.fs_type,
257 'id': self.dlc_id,
258 'image-sha256-hash': image_hash,
259 'image-type': 'dlc',
260 'is-removable': True,
261 'manifest-version': self._MANIFEST_VERSION,
262 'name': self.name,
263 'pre-allocated-size': self.pre_allocated_blocks * self._BLOCK_SIZE,
264 'size': blocks * self._BLOCK_SIZE,
265 'table-sha256-hash': table_hash,
266 'version': self.version,
267 }
268
269 def GenerateVerity(self):
270 """Generate verity parameters and hashes for the image."""
271 with osutils.TempDir(prefix='dlc_') as temp_dir:
272 hash_tree = os.path.join(temp_dir, 'hash_tree')
273 # Get blocks in the image.
274 blocks = math.ceil(
275 os.path.getsize(self.dest_image) / self._BLOCK_SIZE)
276 result = cros_build_lib.RunCommand(
277 ['verity', 'mode=create', 'alg=sha256', 'payload=' + self.dest_image,
278 'payload_blocks=' + str(blocks), 'hashtree=' + hash_tree,
279 'salt=random'], capture_output=True)
280 table = result.output
281
282 # Append the merkle tree to the image.
283 osutils.WriteFile(self.dest_image, osutils.ReadFile(hash_tree), 'a+')
284
285 # Write verity parameter to table file.
286 osutils.WriteFile(self.dest_table, table)
287
288 # Compute image hash.
289 image_hash = HashFile(self.dest_image)
290 table_hash = HashFile(self.dest_table)
291 # Write image hash to imageloader.json file.
292 blocks = math.ceil(
293 os.path.getsize(self.dest_image) / self._BLOCK_SIZE)
294 imageloader_json_content = self.GetImageloaderJsonContent(
295 image_hash, table_hash, int(blocks))
296 with open(self.dest_imageloader_json, 'w') as f:
297 json.dump(imageloader_json_content, f)
298
299 def GenerateDLC(self):
300 """Generate a DLC artifact."""
301 # Create the image and copy the DLC files to it.
302 self.CreateImage()
303 # Generate hash tree and other metadata.
304 self.GenerateVerity()
305
306
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800307def CopyAllDlcs(sysroot, install_root_dir):
308 """Copies all DLC image files into the images directory.
309
310 Copies the DLC image files in the given build directory into the given DLC
311 image directory. If the DLC build directory does not exist, or there is no DLC
312 for that board, this function does nothing.
313
314 Args:
315 sysroot: Path to directory containing DLC images, e.g /build/<board>.
316 install_root_dir: Path to DLC output directory,
317 e.g. src/build/images/<board>/<version>.
318 """
319 output_dir = os.path.join(install_root_dir, 'dlc')
320 build_dir = os.path.join(sysroot, DLC_IMAGE_DIR)
321
322 if not os.path.exists(build_dir) or not os.listdir(build_dir):
Amin Hassani6c0228b2019-03-04 13:42:33 -0800323 logging.info('There is no DLC to copy to output, ignoring.')
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800324 return
325
Amin Hassani6c0228b2019-03-04 13:42:33 -0800326 logging.info('Copying all DLC images to their destination path.')
327 logging.info('Detected the following DLCs: %s',
328 ', '.join(os.listdir(build_dir)))
329
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800330 osutils.SafeMakedirs(output_dir)
331 osutils.CopyDirContents(build_dir, output_dir)
332
Amin Hassani6c0228b2019-03-04 13:42:33 -0800333 logging.info('Done copying the DLCs to their destination.')
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800334
Xiaochu Liudeed0232018-06-26 10:25:34 -0700335def GetParser():
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800336 """Creates an argument parser and returns it."""
Xiaochu Liudeed0232018-06-26 10:25:34 -0700337 parser = commandline.ArgumentParser(description=__doc__)
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800338 # This script is used both for building an individual DLC or copying all final
339 # DLCs images to their final destination nearby chromiumsos_test_image.bin,
340 # etc. These two arguments are required in both cases.
341 parser.add_argument('--sysroot', type='path', metavar='DIR', required=True,
342 help="The root path to the board's build root, e.g. "
343 "/build/eve")
344 parser.add_argument('--install-root-dir', type='path', metavar='DIR',
345 required=True,
346 help='If building a specific DLC, it is the root path to'
347 ' install DLC images (%s) and metadata (%s). Otherwise it'
348 ' is the target directory where the Chrome OS images gets'
349 ' dropped in build_image, e.g. '
350 'src/build/images/<board>/latest.' % (DLC_IMAGE_DIR,
351 DLC_META_DIR))
Amin Hassani22a25eb2019-01-11 14:25:02 -0800352
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800353 one_dlc = parser.add_argument_group('Arguments required for building only '
354 'one DLC')
355 one_dlc.add_argument('--src-dir', type='path', metavar='SRC_DIR_PATH',
356 help='Root directory path that contains all DLC files '
357 'to be packed.')
358 one_dlc.add_argument('--pre-allocated-blocks', type=int,
359 metavar='PREALLOCATEDBLOCKS',
360 help='Number of blocks (block size is 4k) that need to'
361 'be pre-allocated on device.')
362 one_dlc.add_argument('--version', metavar='VERSION', help='DLC Version.')
363 one_dlc.add_argument('--id', metavar='ID', help='DLC ID (unique per DLC).')
364 one_dlc.add_argument('--name', metavar='NAME',
365 help='A human-readable name for the DLC.')
366 one_dlc.add_argument('--fs-type', metavar='FS_TYPE', default=_SQUASHFS_TYPE,
367 choices=(_SQUASHFS_TYPE, _EXT4_TYPE),
368 help='File system type of the image.')
Xiaochu Liudeed0232018-06-26 10:25:34 -0700369 return parser
370
371
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800372def ValidateArguments(opts):
373 """Validates the correctness of the passed arguments.
374
375 Args:
376 opts: Parsed arguments.
377 """
378 # Make sure if the intention is to build one DLC, all the required arguments
379 # are passed.
380 per_dlc_req_args = ('src_dir', 'pre_allocated_blocks', 'version', 'id',
381 'name')
382 if (opts.id and
383 not all(vars(opts)[arg] is not None for arg in per_dlc_req_args)):
384 raise Exception('If the intention is to build only one DLC, all the flags'
385 '%s required for it should be passed .' % per_dlc_req_args)
386
387 if opts.fs_type == _EXT4_TYPE:
388 raise Exception('ext4 unsupported, see https://crbug.com/890060')
389
390
Xiaochu Liudeed0232018-06-26 10:25:34 -0700391def main(argv):
392 opts = GetParser().parse_args(argv)
393 opts.Freeze()
394
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800395 ValidateArguments(opts)
Amin Hassani2af75a92019-01-22 21:07:45 -0800396
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800397 if opts.id:
398 logging.info('Building DLC %s', opts.id)
399 dlc_generator = DlcGenerator(src_dir=opts.src_dir,
400 sysroot=opts.sysroot,
401 install_root_dir=opts.install_root_dir,
402 fs_type=opts.fs_type,
403 pre_allocated_blocks=opts.pre_allocated_blocks,
404 version=opts.version,
405 dlc_id=opts.id,
406 name=opts.name)
407 dlc_generator.GenerateDLC()
408 else:
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800409 CopyAllDlcs(opts.sysroot, opts.install_root_dir)