1#!/usr/bin/python 2# Copyright (c) 2012 The Chromium 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"""This script lays out the PNaCl translator files for a 7 normal Chrome installer, for one platform. Once run num-of-arches times, 8 the result can then be packed into a multi-CRX zip file. 9 10 This script depends on and pulls in the translator nexes and libraries 11 from the toolchain directory (so that must be downloaded first) and 12 it depends on the pnacl_irt_shim. 13""" 14 15import json 16import logging 17import optparse 18import os 19import platform 20import re 21import shutil 22import sys 23 24J = os.path.join 25 26###################################################################### 27# Target arch and build arch junk to convert between all the 28# silly conventions between SCons, Chrome and PNaCl. 29 30# The version of the arch used by NaCl manifest files. 31# This is based on the machine "building" this extension. 32# We also used this to identify the arch-specific different versions of 33# this extension. 34 35def CanonicalArch(arch): 36 if arch in ('x86_64', 'x86-64', 'x64', 'amd64'): 37 return 'x86-64' 38 # TODO(jvoung): be more specific about the arm architecture version? 39 if arch in ('arm', 'armv7'): 40 return 'arm' 41 if re.match('^i.86$', arch) or arch in ('x86_32', 'x86-32', 'ia32', 'x86'): 42 return 'x86-32' 43 return None 44 45def GetBuildArch(): 46 arch = platform.machine() 47 return CanonicalArch(arch) 48 49BUILD_ARCH = GetBuildArch() 50ARCHES = ['x86-32', 'x86-64', 'arm'] 51 52def IsValidArch(arch): 53 return arch in ARCHES 54 55# The version of the arch used by configure and pnacl's build.sh. 56def StandardArch(arch): 57 return {'x86-32': 'i686', 58 'x86-64': 'x86_64', 59 'arm' : 'armv7'}[arch] 60 61 62###################################################################### 63 64def GetNaClRoot(): 65 """ Find the native_client path, relative to this script. 66 This script is in ppapi/... and native_client is a sibling of ppapi. 67 """ 68 script_file = os.path.abspath(__file__) 69 def SearchForNaCl(cur_dir): 70 if cur_dir.endswith('ppapi'): 71 parent = os.path.dirname(cur_dir) 72 sibling = os.path.join(parent, 'native_client') 73 if not os.path.isdir(sibling): 74 raise Exception('Could not find native_client relative to %s' % 75 script_file) 76 return sibling 77 # Detect when we've the root (linux is /, but windows is not...) 78 next_dir = os.path.dirname(cur_dir) 79 if cur_dir == next_dir: 80 raise Exception('Could not find native_client relative to %s' % 81 script_file) 82 return SearchForNaCl(next_dir) 83 84 return SearchForNaCl(script_file) 85 86 87NACL_ROOT = GetNaClRoot() 88 89 90###################################################################### 91 92# Normalize the platform name to be the way SCons finds chrome binaries. 93# This is based on the platform "building" the extension. 94 95def GetBuildPlatform(): 96 if sys.platform == 'darwin': 97 platform = 'mac' 98 elif sys.platform.startswith('linux'): 99 platform = 'linux' 100 elif sys.platform in ('cygwin', 'win32'): 101 platform = 'windows' 102 else: 103 raise Exception('Unknown platform: %s' % sys.platform) 104 return platform 105BUILD_PLATFORM = GetBuildPlatform() 106 107 108def DetermineInstallerArches(target_arch): 109 arch = CanonicalArch(target_arch) 110 if not IsValidArch(arch): 111 raise Exception('Unknown target_arch %s' % target_arch) 112 # On windows, we need x86-32 and x86-64 (assuming non-windows RT). 113 if BUILD_PLATFORM == 'windows': 114 if arch.startswith('x86'): 115 return ['x86-32', 'x86-64'] 116 else: 117 raise Exception('Unknown target_arch on windows w/ target_arch == %s' % 118 target_arch) 119 else: 120 return [arch] 121 122 123###################################################################### 124 125class PnaclPackaging(object): 126 127 package_base = os.path.dirname(__file__) 128 129 # File paths that are set from the command line. 130 pnacl_template = None 131 tool_revisions = None 132 133 # Agreed-upon name for pnacl-specific info. 134 pnacl_json = 'pnacl.json' 135 136 @staticmethod 137 def SetPnaclInfoTemplatePath(path): 138 PnaclPackaging.pnacl_template = path 139 140 @staticmethod 141 def SetToolsRevisionPath(path): 142 PnaclPackaging.tool_revisions = path 143 144 @staticmethod 145 def PnaclToolsRevision(): 146 with open(PnaclPackaging.tool_revisions, 'r') as f: 147 for line in f.read().splitlines(): 148 if line.startswith('PNACL_VERSION'): 149 _, version = line.split('=') 150 # CWS happens to use version quads, so make it a quad too. 151 # However, each component of the quad is limited to 64K max. 152 # Try to handle a bit more. 153 max_version = 2 ** 16 154 version = int(version) 155 version_more = version / max_version 156 version = version % max_version 157 return '0.1.%d.%d' % (version_more, version) 158 raise Exception('Cannot find PNACL_VERSION in TOOL_REVISIONS file: %s' % 159 PnaclPackaging.tool_revisions) 160 161 @staticmethod 162 def GeneratePnaclInfo(target_dir, abi_version, arch): 163 # A note on versions: pnacl_version is the version of translator built 164 # by the NaCl repo, while abi_version is bumped when the NaCl sandbox 165 # actually changes. 166 pnacl_version = PnaclPackaging.PnaclToolsRevision() 167 with open(PnaclPackaging.pnacl_template, 'r') as pnacl_template_fd: 168 pnacl_template = json.load(pnacl_template_fd) 169 out_name = J(target_dir, UseWhitelistedChars(PnaclPackaging.pnacl_json, 170 None)) 171 with open(out_name, 'w') as output_fd: 172 pnacl_template['pnacl-arch'] = arch 173 pnacl_template['pnacl-version'] = pnacl_version 174 json.dump(pnacl_template, output_fd, sort_keys=True, indent=4) 175 176 177###################################################################### 178 179class PnaclDirs(object): 180 toolchain_dir = J(NACL_ROOT, 'toolchain') 181 output_dir = J(toolchain_dir, 'pnacl-package') 182 183 @staticmethod 184 def TranslatorRoot(): 185 return J(PnaclDirs.toolchain_dir, 'pnacl_translator') 186 187 @staticmethod 188 def LibDir(target_arch): 189 return J(PnaclDirs.TranslatorRoot(), 'lib-%s' % target_arch) 190 191 @staticmethod 192 def SandboxedCompilerDir(target_arch): 193 return J(PnaclDirs.toolchain_dir, 194 'pnacl_translator', StandardArch(target_arch), 'bin') 195 196 @staticmethod 197 def SetOutputDir(d): 198 PnaclDirs.output_dir = d 199 200 @staticmethod 201 def OutputDir(): 202 return PnaclDirs.output_dir 203 204 @staticmethod 205 def OutputAllDir(version_quad): 206 return J(PnaclDirs.OutputDir(), version_quad) 207 208 @staticmethod 209 def OutputArchBase(arch): 210 return '%s' % arch 211 212 @staticmethod 213 def OutputArchDir(arch): 214 # Nest this in another directory so that the layout will be the same 215 # as the "all"/universal version. 216 parent_dir = J(PnaclDirs.OutputDir(), PnaclDirs.OutputArchBase(arch)) 217 return (parent_dir, J(parent_dir, PnaclDirs.OutputArchBase(arch))) 218 219 220###################################################################### 221 222def StepBanner(short_desc, long_desc): 223 logging.info("**** %s\t%s", short_desc, long_desc) 224 225 226def Clean(): 227 out_dir = PnaclDirs.OutputDir() 228 StepBanner('CLEAN', 'Cleaning out old packaging: %s' % out_dir) 229 if os.path.isdir(out_dir): 230 shutil.rmtree(out_dir) 231 else: 232 logging.info('Clean skipped -- no previous output directory!') 233 234###################################################################### 235 236def UseWhitelistedChars(orig_basename, arch): 237 """ Make the filename match the pattern expected by nacl_file_host. 238 239 Currently, this assumes there is prefix "pnacl_public_" and 240 that the allowed chars are in the set [a-zA-Z0-9_]. 241 """ 242 if arch: 243 target_basename = 'pnacl_public_%s_%s' % (arch, orig_basename) 244 else: 245 target_basename = 'pnacl_public_%s' % orig_basename 246 result = re.sub(r'[^a-zA-Z0-9_]', '_', target_basename) 247 logging.info('UseWhitelistedChars using: %s' % result) 248 return result 249 250def CopyFlattenDirsAndPrefix(src_dir, arch, dest_dir): 251 """ Copy files from src_dir to dest_dir. 252 253 When copying, also rename the files such that they match the white-listing 254 pattern in chrome/browser/nacl_host/nacl_file_host.cc. 255 """ 256 for (root, dirs, files) in os.walk(src_dir, followlinks=True): 257 for f in files: 258 # Assume a flat directory. 259 assert (f == os.path.basename(f)) 260 full_name = J(root, f) 261 target_name = UseWhitelistedChars(f, arch) 262 shutil.copy(full_name, J(dest_dir, target_name)) 263 264 265def BuildArchForInstaller(version_quad, arch, lib_overrides): 266 """ Build an architecture specific version for the chrome installer. 267 """ 268 target_dir = PnaclDirs.OutputDir() 269 270 StepBanner('BUILD INSTALLER', 271 'Packaging for arch %s in %s' % (arch, target_dir)) 272 273 # Copy llc.nexe and ld.nexe, but with some renaming and directory flattening. 274 CopyFlattenDirsAndPrefix(PnaclDirs.SandboxedCompilerDir(arch), 275 arch, 276 target_dir) 277 278 # Copy native libraries, also with renaming and directory flattening. 279 CopyFlattenDirsAndPrefix(PnaclDirs.LibDir(arch), arch, target_dir) 280 281 # Also copy files from the list of overrides. 282 # This needs the arch tagged onto the name too, like the other files. 283 if arch in lib_overrides: 284 for override in lib_overrides[arch]: 285 override_base = os.path.basename(override) 286 target_name = UseWhitelistedChars(override_base, arch) 287 shutil.copy(override, J(target_dir, target_name)) 288 289 290def BuildInstallerStyle(version_quad, lib_overrides, arches): 291 """ Package the pnacl component for use within the chrome installer 292 infrastructure. These files need to be named in a special way 293 so that white-listing of files is easy. 294 """ 295 StepBanner("BUILD_ALL", "Packaging installer for version: %s" % version_quad) 296 for arch in arches: 297 BuildArchForInstaller(version_quad, arch, lib_overrides) 298 # Generate pnacl info manifest. 299 # Hack around the fact that there may be more than one arch, on Windows. 300 if len(arches) == 1: 301 arches = arches[0] 302 PnaclPackaging.GeneratePnaclInfo(PnaclDirs.OutputDir(), version_quad, arches) 303 304 305###################################################################### 306 307 308def Main(): 309 usage = 'usage: %prog [options] version_arg' 310 parser = optparse.OptionParser(usage) 311 # We may want to accept a target directory to dump it in the usual 312 # output directory (e.g., scons-out). 313 parser.add_option('-c', '--clean', dest='clean', 314 action='store_true', default=False, 315 help='Clean out destination directory first.') 316 parser.add_option('-d', '--dest', dest='dest', 317 help='The destination root for laying out the extension') 318 parser.add_option('-L', '--lib_override', 319 dest='lib_overrides', action='append', default=[], 320 help='Specify path to a fresher native library ' + 321 'that overrides the tarball library with ' + 322 '(arch:libfile) tuple.') 323 parser.add_option('-t', '--target_arch', 324 dest='target_arch', default=None, 325 help='Only generate the chrome installer version for arch') 326 parser.add_option('--info_template_path', 327 dest='info_template_path', default=None, 328 help='Path of the info template file') 329 parser.add_option('--tool_revisions_path', dest='tool_revisions_path', 330 default=None, help='Location of NaCl TOOL_REVISIONS file.') 331 parser.add_option('-v', '--verbose', dest='verbose', default=False, 332 action='store_true', 333 help='Print verbose debug messages.') 334 335 (options, args) = parser.parse_args() 336 if options.verbose: 337 logging.getLogger().setLevel(logging.DEBUG) 338 else: 339 logging.getLogger().setLevel(logging.ERROR) 340 logging.info('pnacl_component_crx_gen w/ options %s and args %s\n' 341 % (options, args)) 342 343 # Set destination directory before doing any cleaning, etc. 344 if options.dest: 345 PnaclDirs.SetOutputDir(options.dest) 346 347 if options.clean: 348 Clean() 349 350 if options.info_template_path: 351 PnaclPackaging.SetPnaclInfoTemplatePath(options.info_template_path) 352 353 if options.tool_revisions_path: 354 PnaclPackaging.SetToolsRevisionPath(options.tool_revisions_path) 355 356 lib_overrides = {} 357 for o in options.lib_overrides: 358 arch, override_lib = o.split(',') 359 arch = CanonicalArch(arch) 360 if not IsValidArch(arch): 361 raise Exception('Unknown arch for -L: %s (from %s)' % (arch, o)) 362 if not os.path.isfile(override_lib): 363 raise Exception('Override native lib not a file for -L: %s (from %s)' % 364 (override_lib, o)) 365 override_list = lib_overrides.get(arch, []) 366 override_list.append(override_lib) 367 lib_overrides[arch] = override_list 368 369 if len(args) != 1: 370 parser.print_help() 371 parser.error('Incorrect number of arguments') 372 373 abi_version = int(args[0]) 374 375 arches = DetermineInstallerArches(options.target_arch) 376 BuildInstallerStyle(abi_version, lib_overrides, arches) 377 return 0 378 379 380if __name__ == '__main__': 381 sys.exit(Main()) 382