1#!/usr/bin/env python3 -B
2# This Source Code Form is subject to the terms of the Mozilla Public
3# License, v. 2.0. If a copy of the MPL was not distributed with this file,
4# You can obtain one at http://mozilla.org/MPL/2.0/.
5
6import argparse
7import enum
8import logging
9import os
10import shutil
11import stat
12import subprocess
13import sys
14from pathlib import Path
15
16logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.INFO)
17
18
19def find_command(names):
20    """Search for command in `names`, and returns the first one that exists.
21    """
22
23    for name in names:
24        path = shutil.which(name)
25        if path is not None:
26            return name
27
28    return None
29
30
31def assert_command(env_var, name):
32    """Assert that the command is not empty
33    The command name comes from either environment variable or find_command.
34    """
35    if not name:
36        logging.error('{} command not found'.format(env_var))
37        sys.exit(1)
38
39
40def parse_version(topsrc_dir):
41    """Parse milestone.txt and return the entire milestone and major version.
42    """
43    milestone_file = topsrc_dir / 'config' / 'milestone.txt'
44    if not milestone_file.is_file():
45        return ('', '', '')
46
47    with milestone_file.open('r') as f:
48        for line in f:
49            line = line.strip()
50            if not line:
51                continue
52            if line.startswith('#'):
53                continue
54
55            v = line.split('.')
56            return tuple((v + ['', ''])[:3])
57
58    return ('', '', '')
59
60
61tmp_dir = Path('/tmp')
62
63tar = os.environ.get('TAR', find_command(['tar']))
64assert_command('TAR', tar)
65
66rsync = os.environ.get('RSYNC', find_command(['rsync']))
67assert_command('RSYNC', rsync)
68
69autoconf = os.environ.get('AUTOCONF', find_command([
70    'autoconf-2.13',
71    'autoconf2.13',
72    'autoconf213',
73]))
74assert_command('AUTOCONF', autoconf)
75
76src_dir = Path(os.environ.get('SRC_DIR', Path(__file__).parent.absolute()))
77mozjs_name = os.environ.get('MOZJS_NAME', 'mozjs')
78staging_dir = Path(os.environ.get('STAGING', tmp_dir / 'mozjs-src-pkg'))
79dist_dir = Path(os.environ.get('DIST', tmp_dir))
80topsrc_dir = src_dir.parent.parent.absolute()
81
82parsed_major_version, parsed_minor_version, parsed_patch_version = parse_version(topsrc_dir)
83
84major_version = os.environ.get('MOZJS_MAJOR_VERSION', parsed_major_version)
85minor_version = os.environ.get('MOZJS_MINOR_VERSION', parsed_minor_version)
86patch_version = os.environ.get('MOZJS_PATCH_VERSION', parsed_patch_version)
87alpha = os.environ.get('MOZJS_ALPHA', '')
88
89version = '{}-{}.{}.{}'.format(mozjs_name,
90                               major_version,
91                               minor_version,
92                               patch_version or alpha or '0')
93target_dir = staging_dir / version
94package_name = '{}.tar.bz2'.format(version)
95package_file = dist_dir / package_name
96tar_opts = ['-jcf']
97
98# Given there might be some external program that reads the following output,
99# use raw `print`, instead of logging.
100print('Environment:')
101print('    TAR = {}'.format(tar))
102print('    RSYNC = {}'.format(rsync))
103print('    AUTOCONF = {}'.format(autoconf))
104print('    STAGING = {}'.format(staging_dir))
105print('    DIST = {}'.format(dist_dir))
106print('    SRC_DIR = {}'.format(src_dir))
107print('    MOZJS_NAME = {}'.format(mozjs_name))
108print('    MOZJS_MAJOR_VERSION = {}'.format(major_version))
109print('    MOZJS_MINOR_VERSION = {}'.format(minor_version))
110print('    MOZJS_PATCH_VERSION = {}'.format(patch_version))
111print('    MOZJS_ALPHA = {}'.format(alpha))
112print('')
113
114rsync_filter_list = """
115# Top-level config and build files
116
117+ /configure.py
118+ /LICENSE
119+ /Makefile.in
120+ /moz.build
121+ /moz.configure
122+ /test.mozbuild
123
124# Additional libraries (optionally) used by SpiderMonkey
125
126+ /mfbt/**
127+ /nsprpub/**
128
129- /intl/icu/source/data
130- /intl/icu/source/test
131- /intl/icu/source/tools
132+ /intl/icu/**
133
134+ /memory/build/**
135+ /memory/moz.build
136+ /memory/mozalloc/**
137
138+ /modules/fdlibm/**
139+ /modules/zlib/**
140
141+ /mozglue/baseprofiler/**
142+ /mozglue/build/**
143+ /mozglue/misc/**
144+ /mozglue/moz.build
145+ /mozglue/static/**
146
147+ /tools/fuzzing/moz.build
148+ /tools/fuzzing/interface/**
149+ /tools/fuzzing/registry/**
150+ /tools/fuzzing/libfuzzer/**
151
152# Build system and dependencies
153
154+ /Cargo.lock
155+ /build/**
156+ /config/**
157+ /python/**
158
159+ /.cargo/config.in
160
161- /third_party/python/gyp
162+ /third_party/python/**
163+ /third_party/rust/**
164
165+ /layout/tools/reftest/reftest/**
166
167+ /testing/mozbase/**
168+ /testing/web-platform/tests/streams/**
169
170+ /toolkit/crashreporter/tools/symbolstore.py
171+ /toolkit/mozapps/installer/package-name.mk
172
173# SpiderMonkey itself
174
175+ /js/src/**
176+ /js/app.mozbuild
177+ /js/*.configure
178+ /js/examples/**
179+ /js/public/**
180+ /js/rust/**
181
182+ */
183- /**
184"""
185
186INSTALL_CONTENT = """\
187Full build documentation for SpiderMonkey is hosted on MDN:
188  https://developer.mozilla.org/en-US/docs/SpiderMonkey/Build_Documentation
189
190Note that the libraries produced by the build system include symbols,
191causing the binaries to be extremely large. It is highly suggested that `strip`
192be run over the binaries before deploying them.
193
194Building with default options may be performed as follows:
195  cd js/src
196  mkdir obj
197  cd obj
198  ../configure
199  make # or mozmake on Windows
200"""
201
202README_CONTENT = """\
203This directory contains SpiderMonkey {major_version}.
204
205This release is based on a revision of Mozilla {major_version}:
206  https://hg.mozilla.org/releases/
207The changes in the patches/ directory were applied.
208
209MDN hosts the latest SpiderMonkey {major_version} release notes:
210  https://developer.mozilla.org/en-US/docs/SpiderMonkey/{major_version}
211""".format(major_version=major_version)
212
213
214def is_mozjs_cargo_member(line):
215    """Checks if the line in workspace.members is mozjs-related
216    """
217
218    return '"js/' in line
219
220
221def is_mozjs_crates_io_local_patch(line):
222    """Checks if the line in patch.crates-io is mozjs-related
223    """
224
225    return 'path = "js' in line
226
227
228def clean():
229    """Remove temporary directory and package file.
230    """
231    logging.info('Cleaning {} and {} ...'.format(package_file, target_dir))
232    package_file.unlink()
233    shutil.rmtree(str(target_dir))
234
235
236def assert_clean():
237    """Assert that target directory does not contain generated files.
238    """
239    makefile_file = target_dir / 'js' / 'src' / 'Makefile'
240    if makefile_file.exists():
241        logging.error('found js/src/Makefile. Please clean before packaging.')
242        sys.exit(1)
243
244
245def create_target_dir():
246    if target_dir.exists():
247        logging.warning('dist tree {} already exists!'.format(target_dir))
248    else:
249        target_dir.mkdir(parents=True)
250
251
252def sync_files():
253    # Output of the command should directly go to stdout/stderr.
254    p = subprocess.Popen([str(rsync),
255                          '--delete-excluded',
256                          '--prune-empty-dirs',
257                          '--quiet',
258                          '--recursive',
259                          '{}/'.format(topsrc_dir),
260                          '{}/'.format(target_dir),
261                          '--filter=. -'],
262                         stdin=subprocess.PIPE)
263
264    p.communicate(rsync_filter_list.encode())
265
266    if p.returncode != 0:
267        sys.exit(p.returncode)
268
269
270def copy_cargo_toml():
271    cargo_toml_file = topsrc_dir / 'Cargo.toml'
272    target_cargo_toml_file = target_dir / 'Cargo.toml'
273
274    with cargo_toml_file.open('r') as f:
275        class State(enum.Enum):
276            BEFORE_MEMBER = 1
277            INSIDE_MEMBER = 2
278            AFTER_MEMBER = 3
279            INSIDE_PATCH = 4
280            AFTER_PATCH = 5
281
282        content = ''
283        state = State.BEFORE_MEMBER
284        for line in f:
285            if state == State.BEFORE_MEMBER:
286                if line.strip() == 'members = [':
287                    state = State.INSIDE_MEMBER
288            elif state == State.INSIDE_MEMBER:
289                if line.strip() == ']':
290                    state = State.AFTER_MEMBER
291                elif not is_mozjs_cargo_member(line):
292                    continue
293            elif state == State.AFTER_MEMBER:
294                if line.strip() == '[patch.crates-io]':
295                    state = State.INSIDE_PATCH
296            elif state == State.INSIDE_PATCH:
297                if line.startswith('['):
298                    state = State.AFTER_PATCH
299                if 'path = ' in line:
300                    if not is_mozjs_crates_io_local_patch(line):
301                        continue
302
303            content += line
304
305    with target_cargo_toml_file.open('w') as f:
306        f.write(content)
307
308
309def generate_configure():
310    """Generate configure files to avoid build dependency on autoconf-2.13
311    """
312
313    src_configure_in_file = topsrc_dir / 'js' / 'src' / 'configure.in'
314    src_old_configure_in_file = topsrc_dir / 'js' / 'src' / 'old-configure.in'
315    dest_configure_file = target_dir / 'js' / 'src' / 'configure'
316    dest_old_configure_file = target_dir / 'js' / 'src' / 'old-configure'
317
318    shutil.copy2(str(src_configure_in_file), str(dest_configure_file),
319                 follow_symlinks=False)
320    st = dest_configure_file.stat()
321    dest_configure_file.chmod(
322        st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
323
324    js_src_dir = topsrc_dir / 'js' / 'src'
325
326    with dest_old_configure_file.open('w') as f:
327        subprocess.run([str(autoconf),
328                        '--localdir={}'.format(js_src_dir),
329                        str(src_old_configure_in_file)],
330                       stdout=f,
331                       check=True)
332
333
334def copy_install():
335    """Copy or create INSTALL.
336    """
337
338    staging_install_file = staging_dir / 'INSTALL'
339    target_install_file = target_dir / 'INSTALL'
340
341    if staging_install_file.exists():
342        shutil.copy2(str(staging_install_file), str(target_install_file))
343    else:
344        with target_install_file.open('w') as f:
345            f.write(INSTALL_CONTENT)
346
347
348def copy_readme():
349    """Copy or create README.
350    """
351
352    staging_readme_file = staging_dir / 'README'
353    target_readme_file = target_dir / 'README'
354
355    if staging_readme_file.exists():
356        shutil.copy2(str(staging_readme_file), str(target_readme_file))
357    else:
358        with target_readme_file.open('w') as f:
359            f.write(README_CONTENT)
360
361
362def copy_patches():
363    """Copy patches dir, if it exists.
364    """
365
366    staging_patches_dir = staging_dir / 'patches'
367    top_patches_dir = topsrc_dir / 'patches'
368    target_patches_dir = target_dir / 'patches'
369
370    if staging_patches_dir.is_dir():
371        shutil.copytree(str(staging_patches_dir), str(target_patches_dir))
372    elif top_patches_dir.is_dir():
373        shutil.copytree(str(top_patches_dir), str(target_patches_dir))
374
375
376def remove_python_cache():
377    """Remove *.pyc and *.pyo files if any.
378    """
379    for f in target_dir.glob('**/*.pyc'):
380        f.unlink()
381    for f in target_dir.glob('**/*.pyo'):
382        f.unlink()
383
384
385def stage():
386    """Stage source tarball content.
387    """
388    logging.info('Staging source tarball in {}...'.format(target_dir))
389
390    create_target_dir()
391    sync_files()
392    copy_cargo_toml()
393    generate_configure()
394    copy_install()
395    copy_readme()
396    copy_patches()
397    remove_python_cache()
398
399
400def create_tar():
401    """Roll the tarball.
402    """
403
404    logging.info('Packaging source tarball at {}...'.format(package_file))
405
406    subprocess.run([str(tar)] + tar_opts + [
407        str(package_file),
408        '-C',
409        str(staging_dir),
410        version
411    ], check=True)
412
413
414def build():
415    assert_clean()
416    stage()
417    create_tar()
418
419
420parser = argparse.ArgumentParser(description="Make SpiderMonkey source package")
421subparsers = parser.add_subparsers(dest='COMMAND')
422subparser_update = subparsers.add_parser('clean',
423                                         help='')
424subparser_update = subparsers.add_parser('build',
425                                         help='')
426args = parser.parse_args()
427
428if args.COMMAND == 'clean':
429    clean()
430elif not args.COMMAND or args.COMMAND == 'build':
431    build()
432