blob: c65413aa3b8d2ac8f2a95a5a147e9c865e358efb [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.
Xiaochu Liudeed0232018-06-26 10:25:34 -07005"""Script to generate a DLC (Downloadable Content) artifact."""
6
Mike Frysinger93e8ffa2019-07-03 20:24:18 -04007from __future__ import division
Xiaochu Liudeed0232018-06-26 10:25:34 -07008from __future__ import print_function
9
10import hashlib
11import json
12import math
13import os
Amin Hassanibc1a4792019-10-24 14:39:57 -070014import re
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/'
Andrew67b5fa72020-02-05 14:14:48 -080025DLC_TMP_META_DIR = 'meta'
26DLC_BUILD_DIR = 'build/rootfs/dlc/'
Amin Hassanid5742d32019-01-22 21:13:34 -080027LSB_RELEASE = 'etc/lsb-release'
Jae Hoon Kim5f411e42020-01-09 13:30:56 -080028DLC_IMAGE = 'dlc.img'
29IMAGELOADER_JSON = 'imageloader.json'
Andrew67b5fa72020-02-05 14:14:48 -080030EBUILD_PARAMETERS = 'ebuild_parameters.json'
Amin Hassanid5742d32019-01-22 21:13:34 -080031
Amin Hassani11a88cf2019-01-29 15:31:24 -080032# This file has major and minor version numbers that the update_engine client
33# supports. These values are needed for generating a delta/full payload.
34UPDATE_ENGINE_CONF = 'etc/update_engine.conf'
35
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -080036_EXTRA_RESOURCES = (UPDATE_ENGINE_CONF,)
Amin Hassani11a88cf2019-01-29 15:31:24 -080037
Amin Hassanid5742d32019-01-22 21:13:34 -080038DLC_ID_KEY = 'DLC_ID'
Amin Hassanib5a48042019-03-18 14:30:51 -070039DLC_PACKAGE_KEY = 'DLC_PACKAGE'
Amin Hassanid5742d32019-01-22 21:13:34 -080040DLC_NAME_KEY = 'DLC_NAME'
Amin Hassani8f1cc0f2019-03-06 15:34:53 -080041DLC_APPID_KEY = 'DLC_RELEASE_APPID'
Amin Hassani2af75a92019-01-22 21:07:45 -080042
Amin Hassani22a25eb2019-01-11 14:25:02 -080043_SQUASHFS_TYPE = 'squashfs'
44_EXT4_TYPE = 'ext4'
45
Amin Hassanibc1a4792019-10-24 14:39:57 -070046MAX_ID_NAME = 40
Amin Hassanid5742d32019-01-22 21:13:34 -080047
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -080048
Xiaochu Liudeed0232018-06-26 10:25:34 -070049def HashFile(file_path):
50 """Calculate the sha256 hash of a file.
51
52 Args:
53 file_path: (str) path to the file.
54
55 Returns:
56 [str]: The sha256 hash of the file.
57 """
58 sha256 = hashlib.sha256()
59 with open(file_path, 'rb') as f:
60 for b in iter(lambda: f.read(2048), b''):
61 sha256.update(b)
62 return sha256.hexdigest()
63
64
Jae Hoon Kim5f411e42020-01-09 13:30:56 -080065def GetValueInJsonFile(json_path, key, default_value=None):
66 """Reads file containing JSON and returns value or default_value for key.
67
68 Args:
69 json_path: (str) File containing JSON.
70 key: (str) The desired key to lookup.
71 default_value: (default:None) The default value returned in case of missing
72 key.
73 """
74 with open(json_path) as fd:
75 return json.load(fd).get(key, default_value)
76
77
Andrew67b5fa72020-02-05 14:14:48 -080078class EbuildParams(object):
79 """Object to store and retrieve DLC ebuild parameters.
80
81 Attributes:
82 dlc_id: (str) DLC ID.
83 dlc_package: (str) DLC package.
84 fs_type: (str) file system type.
85 pre_allocated_blocks: (int) number of blocks pre-allocated on device.
86 version: (str) DLC version.
87 name: (str) DLC name.
Jae Hoon Kim6ef63172020-04-06 12:39:04 -070088 description: (str) DLC description.
Andrew67b5fa72020-02-05 14:14:48 -080089 preload: (bool) allow for preloading DLC.
90 """
91
92 def __init__(self, dlc_id, dlc_package, fs_type, pre_allocated_blocks,
Jae Hoon Kim6ef63172020-04-06 12:39:04 -070093 version, name, description, preload):
Andrew67b5fa72020-02-05 14:14:48 -080094 self.dlc_id = dlc_id
95 self.dlc_package = dlc_package
96 self.fs_type = fs_type
97 self.pre_allocated_blocks = pre_allocated_blocks
98 self.version = version
99 self.name = name
Jae Hoon Kim6ef63172020-04-06 12:39:04 -0700100 self.description = description
Andrew67b5fa72020-02-05 14:14:48 -0800101 self.preload = preload
102
103 def StoreDlcParameters(self, install_root_dir, sudo):
104 """Store DLC parameters defined in the ebuild.
105
106 Store DLC parameters defined in the ebuild in a temporary file so they can
107 be retrieved in the build_image phase.
108
109 Args:
110 install_root_dir: (str) The path to the root installation directory.
111 sudo: (bool) Use sudo to write the file.
112 """
113 ebuild_params_path = EbuildParams.GetParamsPath(install_root_dir,
114 self.dlc_id,
115 self.dlc_package)
116 osutils.WriteFile(ebuild_params_path,
117 json.dumps(self.__dict__),
118 makedirs=True, sudo=sudo)
119
120 @staticmethod
121 def GetParamsPath(install_root_dir, dlc_id, dlc_package):
122 """Get the path to the file storing the ebuild parameters.
123
124 Args:
125 install_root_dir: (str) The path to the root installation directory.
126 dlc_id: (str) DLC ID.
127 dlc_package: (str) DLC package.
128
129 Returns:
130 [str]: Path to |EBUILD_PARAMETERS|.
131 """
132 return os.path.join(install_root_dir, DLC_BUILD_DIR, dlc_id, dlc_package,
133 EBUILD_PARAMETERS)
134
135 @classmethod
Jae Hoon Kim264b8d22020-04-06 11:49:19 -0700136 def LoadEbuildParams(cls, sysroot, dlc_id, dlc_package):
Andrew67b5fa72020-02-05 14:14:48 -0800137 """Read the stored ebuild parameters file and return a class instance.
138
139 Args:
140 dlc_id: (str) DLC ID.
141 dlc_package: (str) DLC package.
142 sysroot: (str) The path to the build root directory.
143
144 Returns:
145 [bool] : True if |ebuild_params_path| exists, False otherwise.
146 """
147 path = cls.GetParamsPath(sysroot, dlc_id, dlc_package)
148 if not os.path.exists(path):
149 return None
150
Jae Hoon Kim6ef63172020-04-06 12:39:04 -0700151 with open(path, 'rb') as fp:
Andrew67b5fa72020-02-05 14:14:48 -0800152 return cls(**json.load(fp))
153
154 def __str__(self):
155 return str(self.__dict__)
156
157
Amin Hassani174eb7e2019-01-18 11:11:24 -0800158class DlcGenerator(object):
Xiaochu Liudeed0232018-06-26 10:25:34 -0700159 """Object to generate DLC artifacts."""
160 # Block size for the DLC image.
161 # We use 4K for various reasons:
162 # 1. it's what imageloader (linux kernel) supports.
163 # 2. it's what verity supports.
164 _BLOCK_SIZE = 4096
165 # Blocks in the initial sparse image.
166 _BLOCKS = 500000
167 # Version of manifest file.
168 _MANIFEST_VERSION = 1
169
Amin Hassanicc7ffce2019-01-11 14:57:52 -0800170 # The DLC root path inside the DLC module.
171 _DLC_ROOT_DIR = 'root'
172
Andrew67b5fa72020-02-05 14:14:48 -0800173 def __init__(self, ebuild_params, sysroot, install_root_dir, src_dir=None):
Xiaochu Liudeed0232018-06-26 10:25:34 -0700174 """Object initializer.
175
176 Args:
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800177 sysroot: (str) The path to the build root directory.
Amin Hassani2af75a92019-01-22 21:07:45 -0800178 install_root_dir: (str) The path to the root installation directory.
Andrew67b5fa72020-02-05 14:14:48 -0800179 ebuild_params: (EbuildParams) Ebuild variables.
180 src_dir: (str) Optional path to the DLC source root directory. When None,
181 the default directory in |DLC_BUILD_DIR| is used.
Xiaochu Liudeed0232018-06-26 10:25:34 -0700182 """
Andrew67b5fa72020-02-05 14:14:48 -0800183 # Use a temporary directory to avoid having to use sudo every time we write
184 # into the build directory.
185 self.temp_root = osutils.TempDir(prefix='dlc', sudo_rm=True)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700186 self.src_dir = src_dir
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800187 self.sysroot = sysroot
Amin Hassani2af75a92019-01-22 21:07:45 -0800188 self.install_root_dir = install_root_dir
Andrew67b5fa72020-02-05 14:14:48 -0800189 self.ebuild_params = ebuild_params
190 # If the client is not overriding the src_dir, use the default one.
191 if not self.src_dir:
192 self.src_dir = os.path.join(self.sysroot, DLC_BUILD_DIR,
193 self.ebuild_params.dlc_id,
194 self.ebuild_params.dlc_package,
195 self._DLC_ROOT_DIR)
Amin Hassani2af75a92019-01-22 21:07:45 -0800196
Andrew67b5fa72020-02-05 14:14:48 -0800197 self.image_dir = os.path.join(self.temp_root.tempdir,
198 DLC_BUILD_DIR,
199 self.ebuild_params.dlc_id,
200 self.ebuild_params.dlc_package)
201
202 self.meta_dir = os.path.join(self.image_dir, DLC_TMP_META_DIR)
Amin Hassani2af75a92019-01-22 21:07:45 -0800203
Xiaochu Liudeed0232018-06-26 10:25:34 -0700204 # Create path for all final artifacts.
Jae Hoon Kim5f411e42020-01-09 13:30:56 -0800205 self.dest_image = os.path.join(self.image_dir, DLC_IMAGE)
Amin Hassani2af75a92019-01-22 21:07:45 -0800206 self.dest_table = os.path.join(self.meta_dir, 'table')
Jae Hoon Kim5f411e42020-01-09 13:30:56 -0800207 self.dest_imageloader_json = os.path.join(self.meta_dir, IMAGELOADER_JSON)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700208
Jae Hoon Kime5b88622019-08-23 11:11:33 -0700209 # Log out the member variable values initially set.
210 logging.debug('Initial internal values of DlcGenerator: %s',
Andrew67b5fa72020-02-05 14:14:48 -0800211 repr({k:str(i) for k, i in self.__dict__.items()}))
212
213 def CopyTempContentsToBuildDir(self):
214 """Copy the temp files to the build directory using sudo."""
Jae Hoon Kim6ef63172020-04-06 12:39:04 -0700215 src = self.temp_root.tempdir.rstrip('/') + '/.'
216 dst = self.install_root_dir
217 logging.info(
218 'Copy files from temporary directory (%s) to build directory (%s).',
219 src, dst)
220 cros_build_lib.sudo_run(['cp', '-dR', src, dst])
Jae Hoon Kime5b88622019-08-23 11:11:33 -0700221
Xiaochu Liudeed0232018-06-26 10:25:34 -0700222 def SquashOwnerships(self, path):
223 """Squash the owernships & permissions for files.
224
225 Args:
226 path: (str) path that contains all files to be processed.
227 """
Mike Frysinger45602c72019-09-22 02:15:11 -0400228 cros_build_lib.sudo_run(['chown', '-R', '0:0', path])
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800229 cros_build_lib.sudo_run([
230 'find', path, '-exec', 'touch', '-h', '-t', '197001010000.00', '{}', '+'
231 ])
Xiaochu Liudeed0232018-06-26 10:25:34 -0700232
233 def CreateExt4Image(self):
234 """Create an ext4 image."""
235 with osutils.TempDir(prefix='dlc_') as temp_dir:
236 mount_point = os.path.join(temp_dir, 'mount_point')
Andrew67b5fa72020-02-05 14:14:48 -0800237 # Create the directory where the image is located if it doesn't exist.
238 osutils.SafeMakedirs(os.path.split(self.dest_image)[0])
Xiaochu Liudeed0232018-06-26 10:25:34 -0700239 # Create a raw image file.
240 with open(self.dest_image, 'w') as f:
241 f.truncate(self._BLOCKS * self._BLOCK_SIZE)
242 # Create an ext4 file system on the raw image.
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800243 cros_build_lib.run([
244 '/sbin/mkfs.ext4', '-b',
245 str(self._BLOCK_SIZE), '-O', '^has_journal', self.dest_image
246 ],
247 capture_output=True)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700248 # Create the mount_point directory.
249 osutils.SafeMakedirs(mount_point)
250 # Mount the ext4 image.
251 osutils.MountDir(self.dest_image, mount_point, mount_opts=('loop', 'rw'))
Amin Hassanicc7ffce2019-01-11 14:57:52 -0800252
Xiaochu Liudeed0232018-06-26 10:25:34 -0700253 try:
Amin Hassani11a88cf2019-01-29 15:31:24 -0800254 self.SetupDlcImageFiles(mount_point)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700255 finally:
256 # Unmount the ext4 image.
257 osutils.UmountDir(mount_point)
258 # Shrink to minimum size.
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800259 cros_build_lib.run(['/sbin/e2fsck', '-y', '-f', self.dest_image],
260 capture_output=True)
261 cros_build_lib.run(['/sbin/resize2fs', '-M', self.dest_image],
262 capture_output=True)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700263
264 def CreateSquashfsImage(self):
265 """Create a squashfs image."""
266 with osutils.TempDir(prefix='dlc_') as temp_dir:
Amin Hassani22a25eb2019-01-11 14:25:02 -0800267 squashfs_root = os.path.join(temp_dir, 'squashfs-root')
Amin Hassani11a88cf2019-01-29 15:31:24 -0800268 self.SetupDlcImageFiles(squashfs_root)
Amin Hassani22a25eb2019-01-11 14:25:02 -0800269
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800270 cros_build_lib.run([
271 'mksquashfs', squashfs_root, self.dest_image, '-4k-align', '-noappend'
272 ],
273 capture_output=True)
Amin Hassani22a25eb2019-01-11 14:25:02 -0800274
275 # We changed the ownership and permissions of the squashfs_root
276 # directory. Now we need to remove it manually.
277 osutils.RmDir(squashfs_root, sudo=True)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700278
Amin Hassani11a88cf2019-01-29 15:31:24 -0800279 def SetupDlcImageFiles(self, dlc_dir):
280 """Prepares the directory dlc_dir with all the files a DLC needs.
281
282 Args:
283 dlc_dir: (str) The path to where to setup files inside the DLC.
284 """
285 dlc_root_dir = os.path.join(dlc_dir, self._DLC_ROOT_DIR)
286 osutils.SafeMakedirs(dlc_root_dir)
Jae Hoon Kimd14646b2019-08-21 14:49:26 -0700287 osutils.CopyDirContents(self.src_dir, dlc_root_dir, symlinks=True)
Amin Hassani11a88cf2019-01-29 15:31:24 -0800288 self.PrepareLsbRelease(dlc_dir)
289 self.CollectExtraResources(dlc_dir)
290 self.SquashOwnerships(dlc_dir)
291
Amin Hassanid5742d32019-01-22 21:13:34 -0800292 def PrepareLsbRelease(self, dlc_dir):
293 """Prepare the file /etc/lsb-release in the DLC module.
294
295 This file is used dropping some identification parameters for the DLC.
296
297 Args:
298 dlc_dir: (str) The path to root directory of the DLC. e.g. mounted point
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800299 when we are creating the image.
Amin Hassanid5742d32019-01-22 21:13:34 -0800300 """
Amin Hassani8f1cc0f2019-03-06 15:34:53 -0800301 # Reading the platform APPID and creating the DLC APPID.
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800302 platform_lsb_release = osutils.ReadFile(
303 os.path.join(self.sysroot, LSB_RELEASE))
Amin Hassani8f1cc0f2019-03-06 15:34:53 -0800304 app_id = None
305 for line in platform_lsb_release.split('\n'):
306 if line.startswith(cros_set_lsb_release.LSB_KEY_APPID_RELEASE):
307 app_id = line.split('=')[1]
308 if app_id is None:
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800309 raise Exception(
310 '%s does not have a valid key %s' %
311 (platform_lsb_release, cros_set_lsb_release.LSB_KEY_APPID_RELEASE))
Amin Hassanid5742d32019-01-22 21:13:34 -0800312
Mike Frysinger1c834ea2019-10-14 04:29:41 -0400313 fields = (
Andrew67b5fa72020-02-05 14:14:48 -0800314 (DLC_ID_KEY, self.ebuild_params.dlc_id),
315 (DLC_PACKAGE_KEY, self.ebuild_params.dlc_package),
316 (DLC_NAME_KEY, self.ebuild_params.name),
Amin Hassani8f1cc0f2019-03-06 15:34:53 -0800317 # The DLC appid is generated by concatenating the platform appid with
318 # the DLC ID using an underscore. This pattern should never be changed
319 # once set otherwise it can break a lot of things!
Andrew67b5fa72020-02-05 14:14:48 -0800320 (DLC_APPID_KEY, '%s_%s' % (app_id, self.ebuild_params.dlc_id)),
Mike Frysinger1c834ea2019-10-14 04:29:41 -0400321 )
Amin Hassani8f1cc0f2019-03-06 15:34:53 -0800322
323 lsb_release = os.path.join(dlc_dir, LSB_RELEASE)
324 osutils.SafeMakedirs(os.path.dirname(lsb_release))
Mike Frysinger1c834ea2019-10-14 04:29:41 -0400325 content = ''.join('%s=%s\n' % (k, v) for k, v in fields)
Amin Hassanid5742d32019-01-22 21:13:34 -0800326 osutils.WriteFile(lsb_release, content)
327
Amin Hassani11a88cf2019-01-29 15:31:24 -0800328 def CollectExtraResources(self, dlc_dir):
329 """Collect the extra resources needed by the DLC module.
330
331 Look at the documentation around _EXTRA_RESOURCES.
332
333 Args:
334 dlc_dir: (str) The path to root directory of the DLC. e.g. mounted point
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800335 when we are creating the image.
Amin Hassani11a88cf2019-01-29 15:31:24 -0800336 """
337 for r in _EXTRA_RESOURCES:
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800338 source_path = os.path.join(self.sysroot, r)
Amin Hassani11a88cf2019-01-29 15:31:24 -0800339 target_path = os.path.join(dlc_dir, r)
340 osutils.SafeMakedirs(os.path.dirname(target_path))
341 shutil.copyfile(source_path, target_path)
342
Xiaochu Liudeed0232018-06-26 10:25:34 -0700343 def CreateImage(self):
344 """Create the image and copy the DLC files to it."""
Jae Hoon Kime5b88622019-08-23 11:11:33 -0700345 logging.info('Creating the DLC image.')
Andrew67b5fa72020-02-05 14:14:48 -0800346 if self.ebuild_params.fs_type == _EXT4_TYPE:
Xiaochu Liudeed0232018-06-26 10:25:34 -0700347 self.CreateExt4Image()
Andrew67b5fa72020-02-05 14:14:48 -0800348 elif self.ebuild_params.fs_type == _SQUASHFS_TYPE:
Xiaochu Liudeed0232018-06-26 10:25:34 -0700349 self.CreateSquashfsImage()
350 else:
Andrew67b5fa72020-02-05 14:14:48 -0800351 raise ValueError('Wrong fs type: %s used:' % self.ebuild_params.fs_type)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700352
Jae Hoon Kime5b88622019-08-23 11:11:33 -0700353 def VerifyImageSize(self):
Xiaochu Liu36b30592019-08-06 09:39:54 -0700354 """Verify the image can fit to the reserved file."""
Jae Hoon Kime5b88622019-08-23 11:11:33 -0700355 logging.info('Verifying the DLC image size.')
356 image_bytes = os.path.getsize(self.dest_image)
Andrew67b5fa72020-02-05 14:14:48 -0800357 preallocated_bytes = (self.ebuild_params.pre_allocated_blocks *
358 self._BLOCK_SIZE)
Xiaochu Liu36b30592019-08-06 09:39:54 -0700359 # Verifies the actual size of the DLC image is NOT smaller than the
360 # preallocated space.
361 if preallocated_bytes < image_bytes:
362 raise ValueError(
363 'The DLC_PREALLOC_BLOCKS (%s) value set in DLC ebuild resulted in a '
364 'max size of DLC_PREALLOC_BLOCKS * 4K (%s) bytes the DLC image is '
365 'allowed to occupy. The value is smaller than the actual image size '
366 '(%s) required. Increase DLC_PREALLOC_BLOCKS in your ebuild to at '
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800367 'least %d.' %
Andrew67b5fa72020-02-05 14:14:48 -0800368 (self.ebuild_params.pre_allocated_blocks, preallocated_bytes,
369 image_bytes, self.GetOptimalImageBlockSize(image_bytes)))
Jae Hoon Kime5b88622019-08-23 11:11:33 -0700370
371 def GetOptimalImageBlockSize(self, image_bytes):
372 """Given the image bytes, get the least amount of blocks required."""
373 return int(math.ceil(image_bytes / self._BLOCK_SIZE))
Xiaochu Liu36b30592019-08-06 09:39:54 -0700374
Xiaochu Liudeed0232018-06-26 10:25:34 -0700375 def GetImageloaderJsonContent(self, image_hash, table_hash, blocks):
376 """Return the content of imageloader.json file.
377
378 Args:
379 image_hash: (str) sha256 hash of the DLC image.
380 table_hash: (str) sha256 hash of the DLC table file.
381 blocks: (int) number of blocks in the DLC image.
382
383 Returns:
384 [str]: content of imageloader.json file.
385 """
386 return {
Andrew67b5fa72020-02-05 14:14:48 -0800387 'fs-type': self.ebuild_params.fs_type,
388 'id': self.ebuild_params.dlc_id,
389 'package': self.ebuild_params.dlc_package,
Xiaochu Liudeed0232018-06-26 10:25:34 -0700390 'image-sha256-hash': image_hash,
391 'image-type': 'dlc',
392 'is-removable': True,
393 'manifest-version': self._MANIFEST_VERSION,
Andrew67b5fa72020-02-05 14:14:48 -0800394 'name': self.ebuild_params.name,
Jae Hoon Kim6ef63172020-04-06 12:39:04 -0700395 'description': self.ebuild_params.description,
Andrew67b5fa72020-02-05 14:14:48 -0800396 'pre-allocated-size':
397 str(self.ebuild_params.pre_allocated_blocks * self._BLOCK_SIZE),
Jae Hoon Kimbd8ae6e2020-02-03 18:54:29 -0800398 'size': str(blocks * self._BLOCK_SIZE),
Xiaochu Liudeed0232018-06-26 10:25:34 -0700399 'table-sha256-hash': table_hash,
Andrew67b5fa72020-02-05 14:14:48 -0800400 'version': self.ebuild_params.version,
401 'preload-allowed': self.ebuild_params.preload,
Xiaochu Liudeed0232018-06-26 10:25:34 -0700402 }
403
404 def GenerateVerity(self):
405 """Generate verity parameters and hashes for the image."""
Jae Hoon Kime5b88622019-08-23 11:11:33 -0700406 logging.info('Generating DLC image verity.')
Xiaochu Liudeed0232018-06-26 10:25:34 -0700407 with osutils.TempDir(prefix='dlc_') as temp_dir:
408 hash_tree = os.path.join(temp_dir, 'hash_tree')
409 # Get blocks in the image.
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800410 blocks = math.ceil(os.path.getsize(self.dest_image) / self._BLOCK_SIZE)
411 result = cros_build_lib.run([
412 'verity', 'mode=create', 'alg=sha256', 'payload=' + self.dest_image,
413 'payload_blocks=' + str(blocks), 'hashtree=' + hash_tree,
414 'salt=random'
415 ],
416 capture_output=True)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700417 table = result.output
418
419 # Append the merkle tree to the image.
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800420 osutils.WriteFile(
421 self.dest_image, osutils.ReadFile(hash_tree, mode='rb'), mode='a+b')
Xiaochu Liudeed0232018-06-26 10:25:34 -0700422
423 # Write verity parameter to table file.
Mike Frysinger1c834ea2019-10-14 04:29:41 -0400424 osutils.WriteFile(self.dest_table, table, mode='wb')
Xiaochu Liudeed0232018-06-26 10:25:34 -0700425
426 # Compute image hash.
427 image_hash = HashFile(self.dest_image)
428 table_hash = HashFile(self.dest_table)
429 # Write image hash to imageloader.json file.
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800430 blocks = math.ceil(os.path.getsize(self.dest_image) / self._BLOCK_SIZE)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700431 imageloader_json_content = self.GetImageloaderJsonContent(
432 image_hash, table_hash, int(blocks))
433 with open(self.dest_imageloader_json, 'w') as f:
434 json.dump(imageloader_json_content, f)
435
436 def GenerateDLC(self):
437 """Generate a DLC artifact."""
Andrew67b5fa72020-02-05 14:14:48 -0800438 # Create directories.
439 osutils.SafeMakedirs(self.image_dir)
440 osutils.SafeMakedirs(self.meta_dir)
441
442 # Create the image into |self.temp_root| and copy the DLC files to it.
Xiaochu Liudeed0232018-06-26 10:25:34 -0700443 self.CreateImage()
Andrew67b5fa72020-02-05 14:14:48 -0800444 # Verify the image created is within pre-allocated size.
Jae Hoon Kime5b88622019-08-23 11:11:33 -0700445 self.VerifyImageSize()
Andrew67b5fa72020-02-05 14:14:48 -0800446 # Generate hash tree and other metadata and save them under
447 # |self.temp_root|.
Xiaochu Liudeed0232018-06-26 10:25:34 -0700448 self.GenerateVerity()
Andrew67b5fa72020-02-05 14:14:48 -0800449 # Copy the files from |self.temp_root| into the build directory.
450 self.CopyTempContentsToBuildDir()
451
452 # Now that the image was successfully generated, delete |ebuild_params_path|
453 # to indicate that the image in the build directory is in sync with the
454 # files installed during the build_package phase.
455 ebuild_params_path = EbuildParams.GetParamsPath(
456 self.sysroot, self.ebuild_params.dlc_id, self.ebuild_params.dlc_package)
457 osutils.SafeUnlink(ebuild_params_path, sudo=True)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700458
459
Andrew67b5fa72020-02-05 14:14:48 -0800460def IsDlcPreloadingAllowed(dlc_id, dlc_build_dir):
Jae Hoon Kim5f411e42020-01-09 13:30:56 -0800461 """Validates that DLC and it's packages all were built with DLC_PRELOAD=true.
462
463 Args:
464 dlc_id: (str) DLC ID.
Andrew67b5fa72020-02-05 14:14:48 -0800465 dlc_build_dir: (str) the root path where DLC build files reside.
Jae Hoon Kim5f411e42020-01-09 13:30:56 -0800466 """
467
Andrew67b5fa72020-02-05 14:14:48 -0800468 dlc_id_meta_dir = os.path.join(dlc_build_dir, dlc_id)
Jae Hoon Kim5f411e42020-01-09 13:30:56 -0800469 if not os.path.exists(dlc_id_meta_dir):
Andrew67b5fa72020-02-05 14:14:48 -0800470 logging.error('DLC build directory (%s) does not exist for preloading '
Jae Hoon Kim5f411e42020-01-09 13:30:56 -0800471 'check, will not preload', dlc_id_meta_dir)
472 return False
473
474 packages = os.listdir(dlc_id_meta_dir)
475 if not packages:
Andrew67b5fa72020-02-05 14:14:48 -0800476 logging.error('DLC ID build directory (%s) does not have any '
Jae Hoon Kim5f411e42020-01-09 13:30:56 -0800477 'packages, will not preload.', dlc_id_meta_dir)
478 return False
479
Andrew67b5fa72020-02-05 14:14:48 -0800480 for package in packages:
481 image_loader_json = os.path.join(dlc_id_meta_dir, package, DLC_TMP_META_DIR,
482 IMAGELOADER_JSON)
483 if not os.path.exists(image_loader_json):
484 logging.error('DLC metadata file (%s) does not exist, will not preload.',
485 image_loader_json)
486 return False
487 if not GetValueInJsonFile(json_path=image_loader_json,
488 key='preload-allowed', default_value=False):
489 return False
490 # All packages support preload.
491 return True
Jae Hoon Kim5f411e42020-01-09 13:30:56 -0800492
493
Andrew67b5fa72020-02-05 14:14:48 -0800494def InstallDlcImages(sysroot, dlc_id=None, install_root_dir=None, preload=False,
495 rootfs=None, src_dir=None):
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800496 """Copies all DLC image files into the images directory.
497
498 Copies the DLC image files in the given build directory into the given DLC
499 image directory. If the DLC build directory does not exist, or there is no DLC
500 for that board, this function does nothing.
501
502 Args:
503 sysroot: Path to directory containing DLC images, e.g /build/<board>.
Andrew67b5fa72020-02-05 14:14:48 -0800504 dlc_id: (str) DLC ID. If None, all the DLCs will be installed.
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800505 install_root_dir: Path to DLC output directory, e.g.
Andrew67b5fa72020-02-05 14:14:48 -0800506 src/build/images/<board>/<version>. If None, the image will be generated
507 but will not be copied to a destination.
Jae Hoon Kim5f411e42020-01-09 13:30:56 -0800508 preload: When true, only copies DLC(s) if built with DLC_PRELOAD=true.
Andrew67b5fa72020-02-05 14:14:48 -0800509 rootfs: (str) Path to the platform rootfs.
510 src_dir: (str) Path to the DLC source root directory.
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800511 """
Andrew67b5fa72020-02-05 14:14:48 -0800512 dlc_build_dir = os.path.join(sysroot, DLC_BUILD_DIR)
513 if not os.path.exists(dlc_build_dir):
514 logging.info('DLC build directory (%s) does not exist, ignoring.',
515 dlc_build_dir)
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800516 return
517
Andrew67b5fa72020-02-05 14:14:48 -0800518 if dlc_id is not None:
519 if not os.path.exists(os.path.join(dlc_build_dir, dlc_id)):
520 raise Exception(
521 'DLC "%s" does not exist in the build directory %s.' %
522 (dlc_id, dlc_build_dir))
523 dlc_ids = [dlc_id]
Jae Hoon Kim5f411e42020-01-09 13:30:56 -0800524 else:
Andrew67b5fa72020-02-05 14:14:48 -0800525 # Process all DLCs.
526 dlc_ids = os.listdir(dlc_build_dir)
527 if not dlc_ids:
528 logging.info('There are no DLC(s) to copy to output, ignoring.')
529 return
Jae Hoon Kim5f411e42020-01-09 13:30:56 -0800530
Andrew67b5fa72020-02-05 14:14:48 -0800531 logging.info('Detected the following DLCs: %s', ', '.join(dlc_ids))
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800532
Andrew67b5fa72020-02-05 14:14:48 -0800533 for d_id in dlc_ids:
534 dlc_id_path = os.path.join(dlc_build_dir, d_id)
535 dlc_packages = [direct for direct in os.listdir(dlc_id_path)
536 if os.path.isdir(os.path.join(dlc_id_path, direct))]
537 for d_package in dlc_packages:
538 logging.info('Building image: DLC %s', d_id)
Jae Hoon Kim264b8d22020-04-06 11:49:19 -0700539 params = EbuildParams.LoadEbuildParams(sysroot=sysroot, dlc_id=d_id,
Jae Hoon Kim6ef63172020-04-06 12:39:04 -0700540 dlc_package=d_package)
Andrew67b5fa72020-02-05 14:14:48 -0800541 # Because portage sandboxes every ebuild package during build_packages
542 # phase, we cannot delete the old image during that phase, but we can use
543 # the existence of the file |EBUILD_PARAMETERS| to know if the image
544 # has to be generated or not.
545 if not params:
546 logging.info('The ebuild parameters file (%s) for DLC (%s) does not '
547 'exist. This means that the image was already '
548 'generated and there is no need to create it again.',
549 EbuildParams.GetParamsPath(sysroot, d_id, d_package), d_id)
550 else:
551 dlc_generator = DlcGenerator(
552 src_dir=src_dir,
553 sysroot=sysroot,
554 install_root_dir=sysroot,
555 ebuild_params=params)
556 dlc_generator.GenerateDLC()
557
558 # Copy the dlc images to install_root_dir.
559 if install_root_dir:
560 if preload and not IsDlcPreloadingAllowed(d_id, dlc_build_dir):
561 logging.info('Skipping installation of DLC %s because the preload '
562 'flag is set and the DLC does not support preloading.',
563 d_id)
564 else:
Jae Hoon Kimb7da6fe2020-04-02 16:17:48 -0700565 osutils.SafeMakedirsNonRoot(install_root_dir)
Andrew67b5fa72020-02-05 14:14:48 -0800566 install_dlc_dir = os.path.join(install_root_dir, d_id, d_package)
Jae Hoon Kimb7da6fe2020-04-02 16:17:48 -0700567 osutils.SafeMakedirsNonRoot(install_dlc_dir)
568 source_dlc_dir = os.path.join(dlc_build_dir, d_id, d_package)
Andrew67b5fa72020-02-05 14:14:48 -0800569 for filepath in (os.path.join(source_dlc_dir, fname) for fname in
570 os.listdir(source_dlc_dir) if
571 fname.endswith('.img')):
572 logging.info('Copying DLC(%s) image from %s to %s: ', d_id,
573 filepath, install_dlc_dir)
Jae Hoon Kimb7da6fe2020-04-02 16:17:48 -0700574 shutil.copy(filepath, install_dlc_dir)
Andrew67b5fa72020-02-05 14:14:48 -0800575 logging.info('Done copying DLC to %s.', install_dlc_dir)
576 else:
577 logging.info('install_root_dir value was not provided. Copying dlc'
578 ' image skipped.')
579
580 # Create metadata directory in rootfs.
581 if rootfs:
582 meta_rootfs = os.path.join(rootfs, DLC_META_DIR, d_id, d_package)
583 osutils.SafeMakedirs(meta_rootfs, sudo=True)
584 # Copy the metadata files to rootfs.
585 meta_dir_src = os.path.join(dlc_build_dir, d_id, d_package,
586 DLC_TMP_META_DIR)
587 logging.info('Copying DLC(%s) metadata from %s to %s: ', d_id,
588 meta_dir_src, meta_rootfs)
589 # Use sudo_run since osutils.CopyDirContents doesn't support sudo.
590 cros_build_lib.sudo_run(['cp', '-dR',
591 meta_dir_src.rstrip('/') + '/.',
592 meta_rootfs], print_cmd=False, stderr=True)
593
594 else:
595 logging.info('rootfs value was not provided. Copying metadata skipped.')
596
597 logging.info('Done installing DLCs.')
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800598
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800599
Xiaochu Liudeed0232018-06-26 10:25:34 -0700600def GetParser():
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800601 """Creates an argument parser and returns it."""
Xiaochu Liudeed0232018-06-26 10:25:34 -0700602 parser = commandline.ArgumentParser(description=__doc__)
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800603 # This script is used both for building an individual DLC or copying all final
604 # DLCs images to their final destination nearby chromiumsos_test_image.bin,
605 # etc. These two arguments are required in both cases.
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800606 parser.add_argument(
607 '--sysroot',
608 type='path',
609 metavar='DIR',
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800610 help="The root path to the board's build root, e.g. "
611 '/build/eve')
Andrew67b5fa72020-02-05 14:14:48 -0800612 # TODO(andrewlassalle): Remove src-dir in the future(2021?) if nobody uses it.
613 parser.add_argument(
614 '--src-dir',
615 type='path',
616 metavar='SRC_DIR_PATH',
617 help='Override the default Root directory path that contains all DLC '
618 'files to be packed.')
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800619 parser.add_argument(
620 '--install-root-dir',
621 type='path',
622 metavar='DIR',
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800623 help='If building a specific DLC, it is the root path to'
624 ' install DLC images (%s) and metadata (%s). Otherwise it'
625 ' is the target directory where the Chrome OS images gets'
626 ' dropped in build_image, e.g. '
Andrew67b5fa72020-02-05 14:14:48 -0800627 'src/build/images/<board>/latest.' % (DLC_BUILD_DIR, DLC_META_DIR))
Amin Hassani22a25eb2019-01-11 14:25:02 -0800628
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800629 one_dlc = parser.add_argument_group('Arguments required for building only '
630 'one DLC')
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800631 one_dlc.add_argument(
Andrew67b5fa72020-02-05 14:14:48 -0800632 '--rootfs',
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800633 type='path',
Andrew67b5fa72020-02-05 14:14:48 -0800634 metavar='ROOT_FS_PATH',
635 help='Path to the platform rootfs.')
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800636 one_dlc.add_argument(
637 '--pre-allocated-blocks',
638 type=int,
639 metavar='PREALLOCATEDBLOCKS',
640 help='Number of blocks (block size is 4k) that need to'
641 'be pre-allocated on device.')
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800642 one_dlc.add_argument('--version', metavar='VERSION', help='DLC Version.')
643 one_dlc.add_argument('--id', metavar='ID', help='DLC ID (unique per DLC).')
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800644 one_dlc.add_argument(
645 '--package',
646 metavar='PACKAGE',
647 help='The package ID that is unique within a DLC, One'
648 ' DLC cannot have duplicate package IDs.')
649 one_dlc.add_argument(
650 '--name', metavar='NAME', help='A human-readable name for the DLC.')
651 one_dlc.add_argument(
Jae Hoon Kim6ef63172020-04-06 12:39:04 -0700652 '--description',
653 help='The description for the DLC.')
654 one_dlc.add_argument(
Jae Hoon Kimfaca4b02020-01-09 13:49:03 -0800655 '--fs-type',
656 metavar='FS_TYPE',
657 default=_SQUASHFS_TYPE,
658 choices=(_SQUASHFS_TYPE, _EXT4_TYPE),
659 help='File system type of the image.')
660 one_dlc.add_argument(
661 '--preload',
662 default=False,
663 action='store_true',
664 help='Allow preloading of DLC.')
Andrew67b5fa72020-02-05 14:14:48 -0800665 one_dlc.add_argument(
666 '--build-package',
667 default=False,
668 action='store_true',
669 help='Flag to indicate if the script is executed during the '
670 'build_packages phase.')
Xiaochu Liudeed0232018-06-26 10:25:34 -0700671 return parser
672
673
Andrew67b5fa72020-02-05 14:14:48 -0800674def ValidateDlcIdentifier(parser, name):
Amin Hassanibc1a4792019-10-24 14:39:57 -0700675 """Validates the DLC identifiers like ID and package names.
676
677 The name specifications are:
678 - No underscore.
679 - First character should be only alphanumeric.
680 - Other characters can be alphanumeric and '-' (dash).
Jae Hoon Kimdb1cab82020-01-21 19:27:28 -0800681 - Maximum length of 40 (MAX_ID_NAME) characters.
Amin Hassanibc1a4792019-10-24 14:39:57 -0700682
683 For more info see:
684 https://chromium.googlesource.com/chromiumos/platform2/+/master/dlcservice/docs/developer.md#create-a-dlc-module
685
686 Args:
Andrew67b5fa72020-02-05 14:14:48 -0800687 parser: Arguments parser.
Jae Hoon Kimdb1cab82020-01-21 19:27:28 -0800688 name: The value of the string to be validated.
Amin Hassanibc1a4792019-10-24 14:39:57 -0700689 """
Jae Hoon Kimdb1cab82020-01-21 19:27:28 -0800690 errors = []
691 if not name:
692 errors.append('Must not be empty.')
693 if not name[0].isalnum():
694 errors.append('Must start with alphanumeric character.')
695 if not re.match(r'^[a-zA-Z0-9][a-zA-Z0-9-]*$', name):
696 errors.append('Must only use alphanumeric and - (dash).')
697 if len(name) > MAX_ID_NAME:
698 errors.append('Must be within %d characters.' % MAX_ID_NAME)
699
700 if errors:
701 msg = '%s is invalid:\n%s' % (name, '\n'.join(errors))
Andrew67b5fa72020-02-05 14:14:48 -0800702 parser.error(msg)
Amin Hassanibc1a4792019-10-24 14:39:57 -0700703
704
Andrew67b5fa72020-02-05 14:14:48 -0800705def ValidateArguments(parser, opts, req_flags, invalid_flags):
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800706 """Validates the correctness of the passed arguments.
707
708 Args:
Andrew67b5fa72020-02-05 14:14:48 -0800709 parser: Arguments parser.
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800710 opts: Parsed arguments.
Andrew67b5fa72020-02-05 14:14:48 -0800711 req_flags: all the required flags.
712 invalid_flags: all the flags that are not allowed.
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800713 """
714 # Make sure if the intention is to build one DLC, all the required arguments
Andrew67b5fa72020-02-05 14:14:48 -0800715 # are passed and none of the invalid ones are passed. This will ensure the
716 # script is called twice per DLC.
717 if opts.id:
718 if not all(vars(opts)[x] is not None for x in req_flags):
719 parser.error('If the intention is to build only one DLC, all the flags'
720 '%s required for it should be passed.' % req_flags)
721 if any(vars(opts)[x] is not None for x in invalid_flags):
722 parser.error('If the intention is to build only one DLC, all the flags'
723 '%s should be passed in the build_packages phase, not in '
724 'the build_image phase.' % invalid_flags)
725
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800726 if opts.fs_type == _EXT4_TYPE:
Andrew67b5fa72020-02-05 14:14:48 -0800727 parser.error('ext4 unsupported for DLC, see https://crbug.com/890060')
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800728
Amin Hassanibc1a4792019-10-24 14:39:57 -0700729 if opts.id:
Andrew67b5fa72020-02-05 14:14:48 -0800730 ValidateDlcIdentifier(parser, opts.id)
Amin Hassanibc1a4792019-10-24 14:39:57 -0700731 if opts.package:
Andrew67b5fa72020-02-05 14:14:48 -0800732 ValidateDlcIdentifier(parser, opts.package)
Amin Hassanibc1a4792019-10-24 14:39:57 -0700733
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800734
Xiaochu Liudeed0232018-06-26 10:25:34 -0700735def main(argv):
Andrew67b5fa72020-02-05 14:14:48 -0800736 parser = GetParser()
737 opts = parser.parse_args(argv)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700738 opts.Freeze()
Andrew67b5fa72020-02-05 14:14:48 -0800739 per_dlc_req_args = ['id']
740 per_dlc_invalid_args = []
741 if opts.build_package:
742 per_dlc_req_args += ['pre_allocated_blocks', 'version', 'name',
Jae Hoon Kim6ef63172020-04-06 12:39:04 -0700743 'description', 'package', 'install_root_dir']
Andrew67b5fa72020-02-05 14:14:48 -0800744 per_dlc_invalid_args += ['src_dir', 'sysroot']
Amin Hassanib97a5ee2019-01-23 14:44:43 -0800745 else:
Andrew67b5fa72020-02-05 14:14:48 -0800746 per_dlc_req_args += ['sysroot']
747 per_dlc_invalid_args += ['name', 'pre_allocated_blocks', 'version',
748 'package']
749
750 ValidateArguments(parser, opts, per_dlc_req_args, per_dlc_invalid_args)
751
752 if opts.build_package:
753 logging.info('Building package: DLC %s', opts.id)
Jae Hoon Kim6ef63172020-04-06 12:39:04 -0700754 params = EbuildParams(
755 dlc_id=opts.id,
756 dlc_package=opts.package,
757 fs_type=opts.fs_type,
758 name=opts.name,
759 description=opts.description,
760 pre_allocated_blocks=opts.pre_allocated_blocks,
761 version=opts.version,
762 preload=opts.preload)
Andrew67b5fa72020-02-05 14:14:48 -0800763 params.StoreDlcParameters(install_root_dir=opts.install_root_dir, sudo=True)
764
765 else:
766 InstallDlcImages(
Jae Hoon Kim5f411e42020-01-09 13:30:56 -0800767 sysroot=opts.sysroot,
Andrew67b5fa72020-02-05 14:14:48 -0800768 dlc_id=opts.id,
Jae Hoon Kim5f411e42020-01-09 13:30:56 -0800769 install_root_dir=opts.install_root_dir,
Andrew67b5fa72020-02-05 14:14:48 -0800770 preload=opts.preload,
771 src_dir=opts.src_dir,
772 rootfs=opts.rootfs)