blob: 8bcd9b75cd583709a665eb32ad3dca3ce4764321 [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
14
15from chromite.lib import commandline
16from chromite.lib import cros_build_lib
17from chromite.lib import osutils
18
19
20def HashFile(file_path):
21 """Calculate the sha256 hash of a file.
22
23 Args:
24 file_path: (str) path to the file.
25
26 Returns:
27 [str]: The sha256 hash of the file.
28 """
29 sha256 = hashlib.sha256()
30 with open(file_path, 'rb') as f:
31 for b in iter(lambda: f.read(2048), b''):
32 sha256.update(b)
33 return sha256.hexdigest()
34
35
36class DLCGenerator(object):
37 """Object to generate DLC artifacts."""
38 # Block size for the DLC image.
39 # We use 4K for various reasons:
40 # 1. it's what imageloader (linux kernel) supports.
41 # 2. it's what verity supports.
42 _BLOCK_SIZE = 4096
43 # Blocks in the initial sparse image.
44 _BLOCKS = 500000
45 # Version of manifest file.
46 _MANIFEST_VERSION = 1
47
Colin Howesd00e9dc2018-10-08 19:48:06 +000048 def __init__(self, dest_dir, src_dir, fs_type, pre_allocated_blocks, version,
49 dlc_id, name):
Xiaochu Liudeed0232018-06-26 10:25:34 -070050 """Object initializer.
51
52 Args:
Colin Howesd00e9dc2018-10-08 19:48:06 +000053 dest_dir: (str) path to the DLC dest root directory.
Xiaochu Liudeed0232018-06-26 10:25:34 -070054 src_dir: (str) path to the DLC source root directory.
55 fs_type: (str) file system type.
56 pre_allocated_blocks: (int) number of blocks pre-allocated on device.
57 version: (str) DLC version.
58 dlc_id: (str) DLC ID.
59 name: (str) DLC name.
60 """
61 self.src_dir = src_dir
62 self.fs_type = fs_type
63 self.pre_allocated_blocks = pre_allocated_blocks
64 self.version = version
65 self.dlc_id = dlc_id
66 self.name = name
67 # Create path for all final artifacts.
Colin Howesd00e9dc2018-10-08 19:48:06 +000068 self.dest_image = os.path.join(dest_dir, 'dlc.img')
69 self.dest_table = os.path.join(dest_dir, 'table')
70 self.dest_imageloader_json = os.path.join(dest_dir, 'imageloader.json')
Xiaochu Liudeed0232018-06-26 10:25:34 -070071
72 def SquashOwnerships(self, path):
73 """Squash the owernships & permissions for files.
74
75 Args:
76 path: (str) path that contains all files to be processed.
77 """
78 cros_build_lib.SudoRunCommand(['chown', '-R', '0:0', path])
79 cros_build_lib.SudoRunCommand(
80 ['find', path, '-exec', 'touch', '-h', '-t', '197001010000.00', '{}',
81 '+'])
82
83 def CreateExt4Image(self):
84 """Create an ext4 image."""
85 with osutils.TempDir(prefix='dlc_') as temp_dir:
86 mount_point = os.path.join(temp_dir, 'mount_point')
87 # Create a raw image file.
88 with open(self.dest_image, 'w') as f:
89 f.truncate(self._BLOCKS * self._BLOCK_SIZE)
90 # Create an ext4 file system on the raw image.
91 cros_build_lib.RunCommand(
92 ['/sbin/mkfs.ext4', '-b', str(self._BLOCK_SIZE), '-O',
93 '^has_journal', self.dest_image], capture_output=True)
94 # Create the mount_point directory.
95 osutils.SafeMakedirs(mount_point)
96 # Mount the ext4 image.
97 osutils.MountDir(self.dest_image, mount_point, mount_opts=('loop', 'rw'))
98 try:
99 # Copy DLC files over to the image.
100 cros_build_lib.SudoRunCommand(['cp', '-a', self.src_dir, mount_point])
101 self.SquashOwnerships(mount_point)
102 finally:
103 # Unmount the ext4 image.
104 osutils.UmountDir(mount_point)
105 # Shrink to minimum size.
106 cros_build_lib.RunCommand(
107 ['/sbin/e2fsck', '-y', '-f', self.dest_image], capture_output=True)
108 cros_build_lib.RunCommand(
109 ['/sbin/resize2fs', '-M', self.dest_image], capture_output=True)
110
111 def CreateSquashfsImage(self):
112 """Create a squashfs image."""
113 with osutils.TempDir(prefix='dlc_') as temp_dir:
114 cros_build_lib.SudoRunCommand(['cp', '-a', self.src_dir, temp_dir])
115 self.SquashOwnerships(temp_dir)
116 cros_build_lib.SudoRunCommand(
117 ['mksquashfs', temp_dir, self.dest_image, '-4k-align', '-noappend'],
118 capture_output=True)
119
120 def CreateImage(self):
121 """Create the image and copy the DLC files to it."""
122 if self.fs_type == 'ext4':
123 self.CreateExt4Image()
124 elif self.fs_type == 'squashfs':
125 self.CreateSquashfsImage()
126 else:
127 raise ValueError('Wrong fs type: %s used:' % self.fs_type)
128
129 def GetImageloaderJsonContent(self, image_hash, table_hash, blocks):
130 """Return the content of imageloader.json file.
131
132 Args:
133 image_hash: (str) sha256 hash of the DLC image.
134 table_hash: (str) sha256 hash of the DLC table file.
135 blocks: (int) number of blocks in the DLC image.
136
137 Returns:
138 [str]: content of imageloader.json file.
139 """
140 return {
141 'fs-type': self.fs_type,
142 'id': self.dlc_id,
143 'image-sha256-hash': image_hash,
144 'image-type': 'dlc',
145 'is-removable': True,
146 'manifest-version': self._MANIFEST_VERSION,
147 'name': self.name,
148 'pre-allocated-size': self.pre_allocated_blocks * self._BLOCK_SIZE,
149 'size': blocks * self._BLOCK_SIZE,
150 'table-sha256-hash': table_hash,
151 'version': self.version,
152 }
153
154 def GenerateVerity(self):
155 """Generate verity parameters and hashes for the image."""
156 with osutils.TempDir(prefix='dlc_') as temp_dir:
157 hash_tree = os.path.join(temp_dir, 'hash_tree')
158 # Get blocks in the image.
159 blocks = math.ceil(
160 os.path.getsize(self.dest_image) / self._BLOCK_SIZE)
161 result = cros_build_lib.RunCommand(
162 ['verity', 'mode=create', 'alg=sha256', 'payload=' + self.dest_image,
163 'payload_blocks=' + str(blocks), 'hashtree=' + hash_tree,
164 'salt=random'], capture_output=True)
165 table = result.output
166
167 # Append the merkle tree to the image.
168 osutils.WriteFile(self.dest_image, osutils.ReadFile(hash_tree), 'a+')
169
170 # Write verity parameter to table file.
171 osutils.WriteFile(self.dest_table, table)
172
173 # Compute image hash.
174 image_hash = HashFile(self.dest_image)
175 table_hash = HashFile(self.dest_table)
176 # Write image hash to imageloader.json file.
177 blocks = math.ceil(
178 os.path.getsize(self.dest_image) / self._BLOCK_SIZE)
179 imageloader_json_content = self.GetImageloaderJsonContent(
180 image_hash, table_hash, int(blocks))
181 with open(self.dest_imageloader_json, 'w') as f:
182 json.dump(imageloader_json_content, f)
183
184 def GenerateDLC(self):
185 """Generate a DLC artifact."""
186 # Create the image and copy the DLC files to it.
187 self.CreateImage()
188 # Generate hash tree and other metadata.
189 self.GenerateVerity()
190
191
192def GetParser():
193 parser = commandline.ArgumentParser(description=__doc__)
194 # Required arguments:
195 required = parser.add_argument_group('Required Arguments')
196 required.add_argument('--src-dir', type='path', metavar='SRC_DIR_PATH',
197 required=True,
198 help='Root directory path that contains all DLC files '
199 'to be packed.')
Colin Howesd00e9dc2018-10-08 19:48:06 +0000200 required.add_argument('--dest-dir', type='path', metavar='DEST_DIR_PATH',
Xiaochu Liudeed0232018-06-26 10:25:34 -0700201 required=True,
Colin Howesd00e9dc2018-10-08 19:48:06 +0000202 help='Root directory path that contains output.')
Xiaochu Liudeed0232018-06-26 10:25:34 -0700203 required.add_argument('--fs-type', metavar='FS_TYPE', required=True,
204 choices=['squashfs', 'ext4'],
205 help='File system type of the image.')
206 required.add_argument('--pre-allocated-blocks', type=int,
207 metavar='PREALLOCATEDBLOCKS', required=True,
208 help='Number of blocks (block size is 4k) that need to'
209 'be pre-allocated on device.')
210 required.add_argument('--version', metavar='VERSION', required=True,
211 help='DLC Version.')
212 required.add_argument('--id', metavar='ID', required=True,
213 help='DLC ID (unique per DLC).')
214 required.add_argument('--name', metavar='NAME', required=True,
215 help='A human-readable name for the DLC.')
216 return parser
217
218
219def main(argv):
220 opts = GetParser().parse_args(argv)
221 opts.Freeze()
222
223 # Generate final DLC files.
Colin Howesd00e9dc2018-10-08 19:48:06 +0000224 dlc_generator = DLCGenerator(opts.dest_dir, opts.src_dir, opts.fs_type,
225 opts.pre_allocated_blocks, opts.version, opts.id,
226 opts.name)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700227 dlc_generator.GenerateDLC()