blob: 03e7a8743d9e00231d90a06aba8905595273fc8d [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
21
Amin Hassani2af75a92019-01-22 21:07:45 -080022DLC_META_DIR = 'opt/google/dlc/'
23DLC_IMAGE_DIR = 'build/rootfs/dlc/'
Amin Hassanid5742d32019-01-22 21:13:34 -080024LSB_RELEASE = 'etc/lsb-release'
25
Amin Hassani11a88cf2019-01-29 15:31:24 -080026# This file has major and minor version numbers that the update_engine client
27# supports. These values are needed for generating a delta/full payload.
28UPDATE_ENGINE_CONF = 'etc/update_engine.conf'
29
30_EXTRA_RESOURCES = (
31 UPDATE_ENGINE_CONF,
32)
33
Amin Hassanid5742d32019-01-22 21:13:34 -080034DLC_ID_KEY = 'DLC_ID'
35DLC_NAME_KEY = 'DLC_NAME'
Amin Hassani2af75a92019-01-22 21:07:45 -080036
Amin Hassani22a25eb2019-01-11 14:25:02 -080037_SQUASHFS_TYPE = 'squashfs'
38_EXT4_TYPE = 'ext4'
39
Amin Hassanid5742d32019-01-22 21:13:34 -080040
Xiaochu Liudeed0232018-06-26 10:25:34 -070041def HashFile(file_path):
42 """Calculate the sha256 hash of a file.
43
44 Args:
45 file_path: (str) path to the file.
46
47 Returns:
48 [str]: The sha256 hash of the file.
49 """
50 sha256 = hashlib.sha256()
51 with open(file_path, 'rb') as f:
52 for b in iter(lambda: f.read(2048), b''):
53 sha256.update(b)
54 return sha256.hexdigest()
55
56
Amin Hassani174eb7e2019-01-18 11:11:24 -080057class DlcGenerator(object):
Xiaochu Liudeed0232018-06-26 10:25:34 -070058 """Object to generate DLC artifacts."""
59 # Block size for the DLC image.
60 # We use 4K for various reasons:
61 # 1. it's what imageloader (linux kernel) supports.
62 # 2. it's what verity supports.
63 _BLOCK_SIZE = 4096
64 # Blocks in the initial sparse image.
65 _BLOCKS = 500000
66 # Version of manifest file.
67 _MANIFEST_VERSION = 1
68
Amin Hassanicc7ffce2019-01-11 14:57:52 -080069 # The DLC root path inside the DLC module.
70 _DLC_ROOT_DIR = 'root'
71
Amin Hassanib97a5ee2019-01-23 14:44:43 -080072 def __init__(self, src_dir, sysroot, install_root_dir, fs_type,
Amin Hassani11a88cf2019-01-29 15:31:24 -080073 pre_allocated_blocks, version, dlc_id, name):
Xiaochu Liudeed0232018-06-26 10:25:34 -070074 """Object initializer.
75
76 Args:
Xiaochu Liudeed0232018-06-26 10:25:34 -070077 src_dir: (str) path to the DLC source root directory.
Amin Hassanib97a5ee2019-01-23 14:44:43 -080078 sysroot: (str) The path to the build root directory.
Amin Hassani2af75a92019-01-22 21:07:45 -080079 install_root_dir: (str) The path to the root installation directory.
Xiaochu Liudeed0232018-06-26 10:25:34 -070080 fs_type: (str) file system type.
81 pre_allocated_blocks: (int) number of blocks pre-allocated on device.
82 version: (str) DLC version.
83 dlc_id: (str) DLC ID.
84 name: (str) DLC name.
85 """
86 self.src_dir = src_dir
Amin Hassanib97a5ee2019-01-23 14:44:43 -080087 self.sysroot = sysroot
Amin Hassani2af75a92019-01-22 21:07:45 -080088 self.install_root_dir = install_root_dir
Xiaochu Liudeed0232018-06-26 10:25:34 -070089 self.fs_type = fs_type
90 self.pre_allocated_blocks = pre_allocated_blocks
91 self.version = version
92 self.dlc_id = dlc_id
93 self.name = name
Amin Hassani2af75a92019-01-22 21:07:45 -080094
95 self.meta_dir = os.path.join(self.install_root_dir, DLC_META_DIR,
96 self.dlc_id)
97 self.image_dir = os.path.join(self.install_root_dir, DLC_IMAGE_DIR,
98 self.dlc_id)
99 osutils.SafeMakedirs(self.meta_dir)
100 osutils.SafeMakedirs(self.image_dir)
101
Xiaochu Liudeed0232018-06-26 10:25:34 -0700102 # Create path for all final artifacts.
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800103 self.dest_image = os.path.join(self.image_dir, self.GetImageFileName())
Amin Hassani2af75a92019-01-22 21:07:45 -0800104 self.dest_table = os.path.join(self.meta_dir, 'table')
105 self.dest_imageloader_json = os.path.join(self.meta_dir, 'imageloader.json')
Xiaochu Liudeed0232018-06-26 10:25:34 -0700106
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800107 def GetImageFileName(self):
108 """Returns the image file name created based on the dlc_id.
109
110 This probably will be replaced by the partition name once we move to a
111 multip-partition DLC.
112
113 Returns:
114 [str]: The image file name for the DLC.
115 """
116 return 'dlc_%s.img' % self.dlc_id
117
Xiaochu Liudeed0232018-06-26 10:25:34 -0700118 def SquashOwnerships(self, path):
119 """Squash the owernships & permissions for files.
120
121 Args:
122 path: (str) path that contains all files to be processed.
123 """
124 cros_build_lib.SudoRunCommand(['chown', '-R', '0:0', path])
125 cros_build_lib.SudoRunCommand(
126 ['find', path, '-exec', 'touch', '-h', '-t', '197001010000.00', '{}',
127 '+'])
128
129 def CreateExt4Image(self):
130 """Create an ext4 image."""
131 with osutils.TempDir(prefix='dlc_') as temp_dir:
132 mount_point = os.path.join(temp_dir, 'mount_point')
133 # Create a raw image file.
134 with open(self.dest_image, 'w') as f:
135 f.truncate(self._BLOCKS * self._BLOCK_SIZE)
136 # Create an ext4 file system on the raw image.
137 cros_build_lib.RunCommand(
138 ['/sbin/mkfs.ext4', '-b', str(self._BLOCK_SIZE), '-O',
139 '^has_journal', self.dest_image], capture_output=True)
140 # Create the mount_point directory.
141 osutils.SafeMakedirs(mount_point)
142 # Mount the ext4 image.
143 osutils.MountDir(self.dest_image, mount_point, mount_opts=('loop', 'rw'))
Amin Hassanicc7ffce2019-01-11 14:57:52 -0800144
Xiaochu Liudeed0232018-06-26 10:25:34 -0700145 try:
Amin Hassani11a88cf2019-01-29 15:31:24 -0800146 self.SetupDlcImageFiles(mount_point)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700147 finally:
148 # Unmount the ext4 image.
149 osutils.UmountDir(mount_point)
150 # Shrink to minimum size.
151 cros_build_lib.RunCommand(
152 ['/sbin/e2fsck', '-y', '-f', self.dest_image], capture_output=True)
153 cros_build_lib.RunCommand(
154 ['/sbin/resize2fs', '-M', self.dest_image], capture_output=True)
155
156 def CreateSquashfsImage(self):
157 """Create a squashfs image."""
158 with osutils.TempDir(prefix='dlc_') as temp_dir:
Amin Hassani22a25eb2019-01-11 14:25:02 -0800159 squashfs_root = os.path.join(temp_dir, 'squashfs-root')
Amin Hassani11a88cf2019-01-29 15:31:24 -0800160 self.SetupDlcImageFiles(squashfs_root)
Amin Hassani22a25eb2019-01-11 14:25:02 -0800161
162 cros_build_lib.RunCommand(['mksquashfs', squashfs_root, self.dest_image,
163 '-4k-align', '-noappend'],
164 capture_output=True)
165
166 # We changed the ownership and permissions of the squashfs_root
167 # directory. Now we need to remove it manually.
168 osutils.RmDir(squashfs_root, sudo=True)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700169
Amin Hassani11a88cf2019-01-29 15:31:24 -0800170 def SetupDlcImageFiles(self, dlc_dir):
171 """Prepares the directory dlc_dir with all the files a DLC needs.
172
173 Args:
174 dlc_dir: (str) The path to where to setup files inside the DLC.
175 """
176 dlc_root_dir = os.path.join(dlc_dir, self._DLC_ROOT_DIR)
177 osutils.SafeMakedirs(dlc_root_dir)
178 osutils.CopyDirContents(self.src_dir, dlc_root_dir)
179 self.PrepareLsbRelease(dlc_dir)
180 self.CollectExtraResources(dlc_dir)
181 self.SquashOwnerships(dlc_dir)
182
Amin Hassanid5742d32019-01-22 21:13:34 -0800183 def PrepareLsbRelease(self, dlc_dir):
184 """Prepare the file /etc/lsb-release in the DLC module.
185
186 This file is used dropping some identification parameters for the DLC.
187
188 Args:
189 dlc_dir: (str) The path to root directory of the DLC. e.g. mounted point
190 when we are creating the image.
191 """
192 lsb_release = os.path.join(dlc_dir, LSB_RELEASE)
193 osutils.SafeMakedirs(os.path.dirname(lsb_release))
194
195 fields = {
196 DLC_ID_KEY: self.dlc_id,
197 DLC_NAME_KEY: self.name,
198 }
199 content = ''.join(['%s=%s\n' % (k, v) for k, v in fields.items()])
200 osutils.WriteFile(lsb_release, content)
201
Amin Hassani11a88cf2019-01-29 15:31:24 -0800202 def CollectExtraResources(self, dlc_dir):
203 """Collect the extra resources needed by the DLC module.
204
205 Look at the documentation around _EXTRA_RESOURCES.
206
207 Args:
208 dlc_dir: (str) The path to root directory of the DLC. e.g. mounted point
209 when we are creating the image.
210 """
211 for r in _EXTRA_RESOURCES:
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800212 source_path = os.path.join(self.sysroot, r)
Amin Hassani11a88cf2019-01-29 15:31:24 -0800213 target_path = os.path.join(dlc_dir, r)
214 osutils.SafeMakedirs(os.path.dirname(target_path))
215 shutil.copyfile(source_path, target_path)
216
Xiaochu Liudeed0232018-06-26 10:25:34 -0700217 def CreateImage(self):
218 """Create the image and copy the DLC files to it."""
Amin Hassani22a25eb2019-01-11 14:25:02 -0800219 if self.fs_type == _EXT4_TYPE:
Xiaochu Liudeed0232018-06-26 10:25:34 -0700220 self.CreateExt4Image()
Amin Hassani22a25eb2019-01-11 14:25:02 -0800221 elif self.fs_type == _SQUASHFS_TYPE:
Xiaochu Liudeed0232018-06-26 10:25:34 -0700222 self.CreateSquashfsImage()
223 else:
224 raise ValueError('Wrong fs type: %s used:' % self.fs_type)
225
226 def GetImageloaderJsonContent(self, image_hash, table_hash, blocks):
227 """Return the content of imageloader.json file.
228
229 Args:
230 image_hash: (str) sha256 hash of the DLC image.
231 table_hash: (str) sha256 hash of the DLC table file.
232 blocks: (int) number of blocks in the DLC image.
233
234 Returns:
235 [str]: content of imageloader.json file.
236 """
237 return {
238 'fs-type': self.fs_type,
239 'id': self.dlc_id,
240 'image-sha256-hash': image_hash,
241 'image-type': 'dlc',
242 'is-removable': True,
243 'manifest-version': self._MANIFEST_VERSION,
244 'name': self.name,
245 'pre-allocated-size': self.pre_allocated_blocks * self._BLOCK_SIZE,
246 'size': blocks * self._BLOCK_SIZE,
247 'table-sha256-hash': table_hash,
248 'version': self.version,
249 }
250
251 def GenerateVerity(self):
252 """Generate verity parameters and hashes for the image."""
253 with osutils.TempDir(prefix='dlc_') as temp_dir:
254 hash_tree = os.path.join(temp_dir, 'hash_tree')
255 # Get blocks in the image.
256 blocks = math.ceil(
257 os.path.getsize(self.dest_image) / self._BLOCK_SIZE)
258 result = cros_build_lib.RunCommand(
259 ['verity', 'mode=create', 'alg=sha256', 'payload=' + self.dest_image,
260 'payload_blocks=' + str(blocks), 'hashtree=' + hash_tree,
261 'salt=random'], capture_output=True)
262 table = result.output
263
264 # Append the merkle tree to the image.
265 osutils.WriteFile(self.dest_image, osutils.ReadFile(hash_tree), 'a+')
266
267 # Write verity parameter to table file.
268 osutils.WriteFile(self.dest_table, table)
269
270 # Compute image hash.
271 image_hash = HashFile(self.dest_image)
272 table_hash = HashFile(self.dest_table)
273 # Write image hash to imageloader.json file.
274 blocks = math.ceil(
275 os.path.getsize(self.dest_image) / self._BLOCK_SIZE)
276 imageloader_json_content = self.GetImageloaderJsonContent(
277 image_hash, table_hash, int(blocks))
278 with open(self.dest_imageloader_json, 'w') as f:
279 json.dump(imageloader_json_content, f)
280
281 def GenerateDLC(self):
282 """Generate a DLC artifact."""
283 # Create the image and copy the DLC files to it.
284 self.CreateImage()
285 # Generate hash tree and other metadata.
286 self.GenerateVerity()
287
288
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800289def CopyAllDlcs(sysroot, install_root_dir):
290 """Copies all DLC image files into the images directory.
291
292 Copies the DLC image files in the given build directory into the given DLC
293 image directory. If the DLC build directory does not exist, or there is no DLC
294 for that board, this function does nothing.
295
296 Args:
297 sysroot: Path to directory containing DLC images, e.g /build/<board>.
298 install_root_dir: Path to DLC output directory,
299 e.g. src/build/images/<board>/<version>.
300 """
301 output_dir = os.path.join(install_root_dir, 'dlc')
302 build_dir = os.path.join(sysroot, DLC_IMAGE_DIR)
303
304 if not os.path.exists(build_dir) or not os.listdir(build_dir):
Amin Hassani6c0228b2019-03-04 13:42:33 -0800305 logging.info('There is no DLC to copy to output, ignoring.')
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800306 return
307
Amin Hassani6c0228b2019-03-04 13:42:33 -0800308 logging.info('Copying all DLC images to their destination path.')
309 logging.info('Detected the following DLCs: %s',
310 ', '.join(os.listdir(build_dir)))
311
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800312 osutils.SafeMakedirs(output_dir)
313 osutils.CopyDirContents(build_dir, output_dir)
314
Amin Hassani6c0228b2019-03-04 13:42:33 -0800315 logging.info('Done copying the DLCs to their destination.')
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800316
Xiaochu Liudeed0232018-06-26 10:25:34 -0700317def GetParser():
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800318 """Creates an argument parser and returns it."""
Xiaochu Liudeed0232018-06-26 10:25:34 -0700319 parser = commandline.ArgumentParser(description=__doc__)
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800320 # This script is used both for building an individual DLC or copying all final
321 # DLCs images to their final destination nearby chromiumsos_test_image.bin,
322 # etc. These two arguments are required in both cases.
323 parser.add_argument('--sysroot', type='path', metavar='DIR', required=True,
324 help="The root path to the board's build root, e.g. "
325 "/build/eve")
326 parser.add_argument('--install-root-dir', type='path', metavar='DIR',
327 required=True,
328 help='If building a specific DLC, it is the root path to'
329 ' install DLC images (%s) and metadata (%s). Otherwise it'
330 ' is the target directory where the Chrome OS images gets'
331 ' dropped in build_image, e.g. '
332 'src/build/images/<board>/latest.' % (DLC_IMAGE_DIR,
333 DLC_META_DIR))
Amin Hassani22a25eb2019-01-11 14:25:02 -0800334
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800335 one_dlc = parser.add_argument_group('Arguments required for building only '
336 'one DLC')
337 one_dlc.add_argument('--src-dir', type='path', metavar='SRC_DIR_PATH',
338 help='Root directory path that contains all DLC files '
339 'to be packed.')
340 one_dlc.add_argument('--pre-allocated-blocks', type=int,
341 metavar='PREALLOCATEDBLOCKS',
342 help='Number of blocks (block size is 4k) that need to'
343 'be pre-allocated on device.')
344 one_dlc.add_argument('--version', metavar='VERSION', help='DLC Version.')
345 one_dlc.add_argument('--id', metavar='ID', help='DLC ID (unique per DLC).')
346 one_dlc.add_argument('--name', metavar='NAME',
347 help='A human-readable name for the DLC.')
348 one_dlc.add_argument('--fs-type', metavar='FS_TYPE', default=_SQUASHFS_TYPE,
349 choices=(_SQUASHFS_TYPE, _EXT4_TYPE),
350 help='File system type of the image.')
Xiaochu Liudeed0232018-06-26 10:25:34 -0700351 return parser
352
353
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800354def ValidateArguments(opts):
355 """Validates the correctness of the passed arguments.
356
357 Args:
358 opts: Parsed arguments.
359 """
360 # Make sure if the intention is to build one DLC, all the required arguments
361 # are passed.
362 per_dlc_req_args = ('src_dir', 'pre_allocated_blocks', 'version', 'id',
363 'name')
364 if (opts.id and
365 not all(vars(opts)[arg] is not None for arg in per_dlc_req_args)):
366 raise Exception('If the intention is to build only one DLC, all the flags'
367 '%s required for it should be passed .' % per_dlc_req_args)
368
369 if opts.fs_type == _EXT4_TYPE:
370 raise Exception('ext4 unsupported, see https://crbug.com/890060')
371
372
Xiaochu Liudeed0232018-06-26 10:25:34 -0700373def main(argv):
374 opts = GetParser().parse_args(argv)
375 opts.Freeze()
376
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800377 ValidateArguments(opts)
Amin Hassani2af75a92019-01-22 21:07:45 -0800378
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800379 if opts.id:
380 logging.info('Building DLC %s', opts.id)
381 dlc_generator = DlcGenerator(src_dir=opts.src_dir,
382 sysroot=opts.sysroot,
383 install_root_dir=opts.install_root_dir,
384 fs_type=opts.fs_type,
385 pre_allocated_blocks=opts.pre_allocated_blocks,
386 version=opts.version,
387 dlc_id=opts.id,
388 name=opts.name)
389 dlc_generator.GenerateDLC()
390 else:
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800391 CopyAllDlcs(opts.sysroot, opts.install_root_dir)