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