blob: 61d4623f1659b3704420b7478da1626d0fd26e2a [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):
305 logging.warn('There is no DLC to copy to output. ignoring!!!')
306 return
307
308 osutils.SafeMakedirs(output_dir)
309 osutils.CopyDirContents(build_dir, output_dir)
310
311
Xiaochu Liudeed0232018-06-26 10:25:34 -0700312def GetParser():
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800313 """Creates an argument parser and returns it."""
Xiaochu Liudeed0232018-06-26 10:25:34 -0700314 parser = commandline.ArgumentParser(description=__doc__)
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800315 # This script is used both for building an individual DLC or copying all final
316 # DLCs images to their final destination nearby chromiumsos_test_image.bin,
317 # etc. These two arguments are required in both cases.
318 parser.add_argument('--sysroot', type='path', metavar='DIR', required=True,
319 help="The root path to the board's build root, e.g. "
320 "/build/eve")
321 parser.add_argument('--install-root-dir', type='path', metavar='DIR',
322 required=True,
323 help='If building a specific DLC, it is the root path to'
324 ' install DLC images (%s) and metadata (%s). Otherwise it'
325 ' is the target directory where the Chrome OS images gets'
326 ' dropped in build_image, e.g. '
327 'src/build/images/<board>/latest.' % (DLC_IMAGE_DIR,
328 DLC_META_DIR))
Amin Hassani22a25eb2019-01-11 14:25:02 -0800329
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800330 one_dlc = parser.add_argument_group('Arguments required for building only '
331 'one DLC')
332 one_dlc.add_argument('--src-dir', type='path', metavar='SRC_DIR_PATH',
333 help='Root directory path that contains all DLC files '
334 'to be packed.')
335 one_dlc.add_argument('--pre-allocated-blocks', type=int,
336 metavar='PREALLOCATEDBLOCKS',
337 help='Number of blocks (block size is 4k) that need to'
338 'be pre-allocated on device.')
339 one_dlc.add_argument('--version', metavar='VERSION', help='DLC Version.')
340 one_dlc.add_argument('--id', metavar='ID', help='DLC ID (unique per DLC).')
341 one_dlc.add_argument('--name', metavar='NAME',
342 help='A human-readable name for the DLC.')
343 one_dlc.add_argument('--fs-type', metavar='FS_TYPE', default=_SQUASHFS_TYPE,
344 choices=(_SQUASHFS_TYPE, _EXT4_TYPE),
345 help='File system type of the image.')
Xiaochu Liudeed0232018-06-26 10:25:34 -0700346 return parser
347
348
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800349def ValidateArguments(opts):
350 """Validates the correctness of the passed arguments.
351
352 Args:
353 opts: Parsed arguments.
354 """
355 # Make sure if the intention is to build one DLC, all the required arguments
356 # are passed.
357 per_dlc_req_args = ('src_dir', 'pre_allocated_blocks', 'version', 'id',
358 'name')
359 if (opts.id and
360 not all(vars(opts)[arg] is not None for arg in per_dlc_req_args)):
361 raise Exception('If the intention is to build only one DLC, all the flags'
362 '%s required for it should be passed .' % per_dlc_req_args)
363
364 if opts.fs_type == _EXT4_TYPE:
365 raise Exception('ext4 unsupported, see https://crbug.com/890060')
366
367
Xiaochu Liudeed0232018-06-26 10:25:34 -0700368def main(argv):
369 opts = GetParser().parse_args(argv)
370 opts.Freeze()
371
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800372 ValidateArguments(opts)
Amin Hassani2af75a92019-01-22 21:07:45 -0800373
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800374 if opts.id:
375 logging.info('Building DLC %s', opts.id)
376 dlc_generator = DlcGenerator(src_dir=opts.src_dir,
377 sysroot=opts.sysroot,
378 install_root_dir=opts.install_root_dir,
379 fs_type=opts.fs_type,
380 pre_allocated_blocks=opts.pre_allocated_blocks,
381 version=opts.version,
382 dlc_id=opts.id,
383 name=opts.name)
384 dlc_generator.GenerateDLC()
385 else:
386 logging.info('Copying all DLC images to their destination path.')
387 CopyAllDlcs(opts.sysroot, opts.install_root_dir)