1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this
3# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5from __future__ import absolute_import, print_function, unicode_literals
6
7import os
8import struct
9import subprocess
10from mozpack.errors import errors
11
12MACHO_SIGNATURES = [
13    0xFEEDFACE,  # mach-o 32-bits big endian
14    0xCEFAEDFE,  # mach-o 32-bits little endian
15    0xFEEDFACF,  # mach-o 64-bits big endian
16    0xCFFAEDFE,  # mach-o 64-bits little endian
17]
18
19FAT_SIGNATURE = 0xCAFEBABE  # mach-o FAT binary
20
21ELF_SIGNATURE = 0x7F454C46  # Elf binary
22
23UNKNOWN = 0
24MACHO = 1
25ELF = 2
26
27
28def get_type(path):
29    """
30    Check the signature of the give file and returns what kind of executable
31    matches.
32    """
33    with open(path, "rb") as f:
34        signature = f.read(4)
35        if len(signature) < 4:
36            return UNKNOWN
37        signature = struct.unpack(">L", signature)[0]
38        if signature == ELF_SIGNATURE:
39            return ELF
40        if signature in MACHO_SIGNATURES:
41            return MACHO
42        if signature != FAT_SIGNATURE:
43            return UNKNOWN
44        # We have to sanity check the second four bytes, because Java class
45        # files use the same magic number as Mach-O fat binaries.
46        # This logic is adapted from file(1), which says that Mach-O uses
47        # these bytes to count the number of architectures within, while
48        # Java uses it for a version number. Conveniently, there are only
49        # 18 labelled Mach-O architectures, and Java's first released
50        # class format used the version 43.0.
51        num = f.read(4)
52        if len(num) < 4:
53            return UNKNOWN
54        num = struct.unpack(">L", num)[0]
55        if num < 20:
56            return MACHO
57        return UNKNOWN
58
59
60def is_executable(path):
61    """
62    Return whether a given file path points to an executable or a library,
63    where an executable or library is identified by:
64        - the file extension on OS/2 and WINNT
65        - the file signature on OS/X and ELF systems (GNU/Linux, Android, BSD,
66          Solaris)
67
68    As this function is intended for use to choose between the ExecutableFile
69    and File classes in FileFinder, and choosing ExecutableFile only matters
70    on OS/2, OS/X, ELF and WINNT (in GCC build) systems, we don't bother
71    detecting other kind of executables.
72    """
73    from buildconfig import substs
74
75    if not os.path.exists(path):
76        return False
77
78    if substs["OS_ARCH"] == "WINNT":
79        return path.lower().endswith((substs["DLL_SUFFIX"], substs["BIN_SUFFIX"]))
80
81    return get_type(path) != UNKNOWN
82
83
84def may_strip(path):
85    """
86    Return whether strip() should be called
87    """
88    from buildconfig import substs
89
90    # Bug 1658632: clang-11-based strip complains about d3dcompiler_47.dll.
91    # It's not clear why this happens, but as a quick fix just avoid stripping
92    # this DLL. It's not from our build anyway.
93    if "d3dcompiler" in path:
94        return False
95    return bool(substs.get("PKG_STRIP"))
96
97
98def strip(path):
99    """
100    Execute the STRIP command with STRIP_FLAGS on the given path.
101    """
102    from buildconfig import substs
103
104    strip = substs["STRIP"]
105    flags = substs.get("STRIP_FLAGS", [])
106    cmd = [strip] + flags + [path]
107    if subprocess.call(cmd) != 0:
108        errors.fatal("Error executing " + " ".join(cmd))
109
110
111def may_elfhack(path):
112    """
113    Return whether elfhack() should be called
114    """
115    # elfhack only supports libraries. We should check the ELF header for
116    # the right flag, but checking the file extension works too.
117    from buildconfig import substs
118
119    return (
120        "USE_ELF_HACK" in substs
121        and substs["USE_ELF_HACK"]
122        and path.endswith(substs["DLL_SUFFIX"])
123        and "COMPILE_ENVIRONMENT" in substs
124        and substs["COMPILE_ENVIRONMENT"]
125    )
126
127
128def elfhack(path):
129    """
130    Execute the elfhack command on the given path.
131    """
132    from buildconfig import topobjdir
133
134    cmd = [os.path.join(topobjdir, "build/unix/elfhack/elfhack"), path]
135    if subprocess.call(cmd) != 0:
136        errors.fatal("Error executing " + " ".join(cmd))
137
138
139def xz_compress(path):
140    """
141    Execute xz to compress the given path.
142    """
143    if open(path, "rb").read(5)[1:] == "7zXZ":
144        print("%s is already compressed" % path)
145        return
146
147    from buildconfig import substs
148
149    xz = substs.get("XZ")
150    cmd = [xz, "-zkf", path]
151
152    # For now, the mozglue XZStream ELF loader can only support xz files
153    # with a single stream that contains a single block. In xz, there is no
154    # explicit option to set the max block count. Instead, we force xz to use
155    # single thread mode, which results in a single block.
156    cmd.extend(["--threads=1"])
157
158    bcj = None
159    if substs.get("MOZ_THUMB2"):
160        bcj = "--armthumb"
161    elif substs.get("CPU_ARCH") == "arm":
162        bcj = "--arm"
163    elif substs.get("CPU_ARCH") == "x86":
164        bcj = "--x86"
165
166    if bcj:
167        cmd.extend([bcj])
168
169    # We need to explicitly specify the LZMA filter chain to ensure consistent builds
170    # across platforms. Note that the dict size must be less then 16MiB per the hardcoded
171    # value in mozglue/linker/XZStream.cpp. This is the default LZMA filter chain for for
172    # xz-utils version 5.0. See:
173    # https://github.com/xz-mirror/xz/blob/v5.0.0/src/liblzma/lzma/lzma_encoder_presets.c
174    # https://github.com/xz-mirror/xz/blob/v5.0.0/src/liblzma/api/lzma/container.h#L31
175    cmd.extend(["--lzma2=dict=8MiB,lc=3,lp=0,pb=2,mode=normal,nice=64,mf=bt4,depth=0"])
176    print("xz-compressing %s with %s" % (path, " ".join(cmd)))
177
178    if subprocess.call(cmd) != 0:
179        errors.fatal("Error executing " + " ".join(cmd))
180        return
181
182    os.rename(path + ".xz", path)
183