tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

create_rc.py (12476B)


      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 
      5 import io
      6 import os
      7 from argparse import ArgumentParser
      8 from datetime import datetime
      9 
     10 import buildconfig
     11 from mozbuild.makeutil import Makefile
     12 from mozbuild.preprocessor import Preprocessor
     13 from variables import get_buildid
     14 
     15 TEMPLATE = """
     16 // This Source Code Form is subject to the terms of the Mozilla Public
     17 // License, v. 2.0. If a copy of the MPL was not distributed with this
     18 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
     19 
     20 #include<winuser.h>
     21 #include<winver.h>
     22 
     23 // Note: if you contain versioning information in an included
     24 // RC script, it will be discarded
     25 // Use module.ver to explicitly set these values
     26 
     27 // Do not edit this file. Changes won't affect the build.
     28 
     29 {include}
     30 
     31 Identity LimitedAccessFeature {{ L"{lafidentity}_pcsmm0jrprpb2" }}
     32 
     33 
     34 /////////////////////////////////////////////////////////////////////////////
     35 //
     36 // Version
     37 //
     38 
     39 1 VERSIONINFO
     40 FILEVERSION    {fileversion}
     41 PRODUCTVERSION {productversion}
     42 FILEFLAGSMASK 0x3fL
     43 FILEFLAGS {fileflags}
     44 FILEOS VOS__WINDOWS32
     45 FILETYPE VFT_DLL
     46 FILESUBTYPE 0x0L
     47 BEGIN
     48    BLOCK "StringFileInfo"
     49    BEGIN
     50        BLOCK "000004b0"
     51        BEGIN
     52            VALUE "Comments", "{comment}"
     53            VALUE "LegalCopyright", "{copyright}"
     54            VALUE "CompanyName", "{company}"
     55            VALUE "FileDescription", "{description}"
     56            VALUE "FileVersion", "{mfversion}"
     57            VALUE "ProductVersion", "{mpversion}"
     58            VALUE "InternalName", "{module}"
     59            VALUE "LegalTrademarks", "{trademarks}"
     60            VALUE "OriginalFilename", "{binary}"
     61            VALUE "ProductName", "{productname}"
     62            VALUE "BuildID", "{buildid}"
     63        END
     64    END
     65    BLOCK "VarFileInfo"
     66    BEGIN
     67        VALUE "Translation", 0x0, 1200
     68    END
     69 END
     70 
     71 """
     72 
     73 
     74 class SystemClockDiscrepancy(Exception):
     75    """Represents an error encountered during the build when determining delta between the build time and
     76    the commit time of milestone.txt via VCS."""
     77 
     78 
     79 def preprocess(path, defines):
     80    pp = Preprocessor(defines=defines, marker="%")
     81    pp.context.update(defines)
     82    pp.out = io.StringIO()
     83    pp.do_filter("substitution")
     84    pp.do_include(open(path, encoding="latin1"))
     85    pp.out.seek(0)
     86    return pp.out
     87 
     88 
     89 def parse_module_ver(path, defines):
     90    result = {}
     91    for line in preprocess(path, defines):
     92        content, *comment = line.split("#", 1)
     93        if not content.strip():
     94            continue
     95        entry, value = content.split("=", 1)
     96        result[entry.strip()] = value.strip()
     97    return result
     98 
     99 
    100 def last_winversion_segment(buildid, app_version_display):
    101    """
    102    The last segment needs to fit into a 16 bit number. We also need to
    103    encode what channel this version is from. We'll do this by using 2 bits
    104    to encode the channel, and 14 bits to encode the number of hours since
    105    the 'config/milestone.txt' was modified (relative to the build time).
    106 
    107    This gives us about ~682 days of release hours that will yield a unique
    108    file version for a specific channel/milestone combination. This should suffice
    109    since the main problem we're trying to address is uniqueness in CI for a
    110    channel/milestone over about a 1 month period.
    111 
    112    If two builds for the same channel/milestone are done in CI within the same
    113    hour there's still a chance for overlap and issues with AV as originally
    114    reported in https://bugzilla.mozilla.org/show_bug.cgi?id=1872242
    115 
    116    If a build is done after the ~682 day window of uniqueness, the value for
    117    this segment will always be the maximum value for the channel (no overflow).
    118    It will also always be the maximum value for the channel if a build is done
    119    from a source distribution, because we cannot determine the milestone date
    120    change without a VCS.
    121 
    122    If necessary, you can decode the result of this function. You just need to
    123    do integer division and divide it by 4. The quotient will be the delta
    124    between the milestone bump and the build time, and the remainder will be
    125    the channel digit. Refer to the if/else chain near the end of the function
    126    for what channels the channel digits map to.
    127 
    128    Example:
    129        Encoded: 1544
    130 
    131        1554 / 4 =
    132        Quotient: 388
    133        Remainder: 2 (ESR)
    134    """
    135    from mozversioncontrol import MissingVCSTool, get_repository_object
    136 
    137    # Max 16 bit value with 2 most significant bits as 0 (reserved so we can
    138    # shift later and make room for the channel digit).
    139    MAX_VALUE = 0x3FFF
    140 
    141    try:
    142        import time
    143        from datetime import timedelta, timezone
    144        from pathlib import Path
    145 
    146        topsrcdir = buildconfig.topsrcdir
    147        repo = get_repository_object(topsrcdir)
    148 
    149        milestone_time = repo.get_last_modified_time_for_file(
    150            Path(topsrcdir) / "config" / "milestone.txt"
    151        )
    152        # The buildid doesn't include timezone info, but the milestone_time does.
    153        # We're building on this machine, so we just need the system local timezone
    154        # added to a buildid constructed datetime object to make a valid comparison.
    155        local_tz = timezone(timedelta(seconds=time.timezone))
    156        buildid_time = datetime.strptime(buildid, "%Y%m%d%H%M%S").replace(
    157            tzinfo=local_tz
    158        )
    159 
    160        time_delta = buildid_time - milestone_time
    161        # If the time delta is negative it means that the system clock on the build machine is
    162        # significantly far ahead. If we're in CI we'll raise an error, since this number mostly
    163        # only matters for doing releases in CI. If we're not in CI, we'll just set the value to
    164        # the maximum instead of needlessly interrupting the build of a user with fast/intentionally
    165        # modified system clock.
    166        if time_delta.total_seconds() < 0:
    167            if "MOZ_AUTOMATION" in os.environ:
    168                raise SystemClockDiscrepancy(
    169                    f"The system clock is ahead of the milestone.txt commit time "
    170                    f"by at least {int(time_delta.total_seconds())} seconds (Since "
    171                    f"the milestone commit must come before the build starts). This "
    172                    f"is a problem because use a relative time difference to determine the"
    173                    f"file_version (and it can't be negative), so we cannot proceed. \n\n"
    174                    f"Please ensure the system clock is correct."
    175                )
    176            else:
    177                hours_from_milestone_date = MAX_VALUE
    178        else:
    179            # Convert from seconds to hours
    180            # When a build is done more than ~682 days in the future, we can't represent the value.
    181            # We'll always set the value to the maximum value instead of overflowing.
    182            hours_from_milestone_date = min(
    183                int(time_delta.total_seconds() / 3600), MAX_VALUE
    184            )
    185    except MissingVCSTool:
    186        # If we're here we can't use the VCS to determine the time differential, so
    187        # we'll just set it to the maximum value instead of doing something weird.
    188        hours_from_milestone_date = MAX_VALUE
    189        pass
    190 
    191    if buildconfig.substs.get("NIGHTLY_BUILD"):
    192        # Nightly
    193        channel_digit = 0
    194    elif "b" in app_version_display:
    195        # Beta
    196        channel_digit = 1
    197    elif buildconfig.substs.get("MOZ_ESR"):
    198        # ESR
    199        channel_digit = 2
    200    else:
    201        # Release
    202        channel_digit = 3
    203    # left shift to make room to encode the channel digit
    204    return str((hours_from_milestone_date << 2) + channel_digit)
    205 
    206 
    207 def digits_only(s):
    208    for l in range(len(s), 0, -1):
    209        if s[:l].isdigit():
    210            return s[:l]
    211    return "0"
    212 
    213 
    214 def split_and_normalize_version(version, len):
    215    return ([digits_only(x) for x in version.split(".")] + ["0"] * len)[:len]
    216 
    217 
    218 def has_manifest(module_rc, manifest_id):
    219    for lineFromInput in module_rc.splitlines():
    220        line = lineFromInput.split(None, 2)
    221        if len(line) < 2:
    222            continue
    223        id, what, *rest = line
    224        if id == manifest_id and what in ("24", "RT_MANIFEST"):
    225            return True
    226    return False
    227 
    228 
    229 def generate_module_rc():
    230 
    231    parser = ArgumentParser()
    232    parser.add_argument(
    233        "binary", help="Binary for which the resource file is generated"
    234    )
    235    parser.add_argument("--include", help="Included resources")
    236    parser.add_argument("--dep-file", help="Path to the dependency file")
    237    args = parser.parse_args()
    238 
    239    binary = args.binary
    240    rcinclude = args.include
    241    dep_file = args.dep_file
    242 
    243    deps = set()
    244    extra_deps = set()
    245    buildid = get_buildid()
    246    milestone = buildconfig.substs["GRE_MILESTONE"]
    247    app_version = buildconfig.substs.get("MOZ_APP_VERSION") or milestone
    248    app_version_display = buildconfig.substs.get("MOZ_APP_VERSION_DISPLAY")
    249    app_winversion = ",".join(split_and_normalize_version(app_version, 4))
    250    milestone_winversion = ",".join(
    251        split_and_normalize_version(milestone, 3)
    252        + [last_winversion_segment(buildid, app_version_display)]
    253    )
    254    display_name = buildconfig.substs.get("MOZ_APP_DISPLAYNAME", "Mozilla")
    255 
    256    milestone_string = milestone
    257 
    258    flags = ["0"]
    259    if buildconfig.substs.get("MOZ_DEBUG"):
    260        flags.append("VS_FF_DEBUG")
    261        milestone_string += " Debug"
    262    if not buildconfig.substs.get("MOZILLA_OFFICIAL"):
    263        flags.append("VS_FF_PRIVATEBUILD")
    264    if buildconfig.substs.get("NIGHTLY_BUILD"):
    265        flags.append("VS_FF_PRERELEASE")
    266 
    267    defines = {
    268        "MOZ_APP_DISPLAYNAME": display_name,
    269        "MOZ_APP_VERSION": app_version,
    270        "MOZ_APP_WINVERSION": app_winversion,
    271    }
    272 
    273    relobjdir = os.path.relpath(".", buildconfig.topobjdir)
    274    srcdir = os.path.join(buildconfig.topsrcdir, relobjdir)
    275    module_ver = os.path.join(srcdir, "module.ver")
    276    if os.path.exists(module_ver):
    277        deps.add(module_ver)
    278        overrides = parse_module_ver(module_ver, defines)
    279    else:
    280        overrides = {}
    281 
    282    if rcinclude:
    283        include = f"// From included resource {rcinclude}\n{preprocess(rcinclude, defines).read()}"
    284    else:
    285        include = ""
    286 
    287    # Set the identity field for the Limited Access Feature
    288    # Must match the tokens used in Win11LimitedAccessFeatures.cpp
    289    lafidentity = "MozillaFirefox"
    290    # lafidentity = "FirefoxBeta"
    291    # lafidentity = "FirefoxNightly"
    292 
    293    data = TEMPLATE.format(
    294        include=include,
    295        lafidentity=lafidentity,
    296        fileversion=overrides.get("WIN32_MODULE_FILEVERSION", milestone_winversion),
    297        productversion=overrides.get(
    298            "WIN32_MODULE_PRODUCTVERSION", milestone_winversion
    299        ),
    300        fileflags=" | ".join(flags),
    301        comment=overrides.get("WIN32_MODULE_COMMENT", ""),
    302        copyright=overrides.get("WIN32_MODULE_COPYRIGHT", "License: MPL 2"),
    303        company=overrides.get("WIN32_MODULE_COMPANYNAME", "Mozilla Foundation"),
    304        description=overrides.get("WIN32_MODULE_DESCRIPTION", ""),
    305        mfversion=overrides.get("WIN32_MODULE_FILEVERSION_STRING", milestone_string),
    306        mpversion=overrides.get("WIN32_MODULE_PRODUCTVERSION_STRING", milestone_string),
    307        module=overrides.get("WIN32_MODULE_NAME", ""),
    308        trademarks=overrides.get("WIN32_MODULE_TRADEMARKS", "Mozilla"),
    309        binary=overrides.get("WIN32_MODULE_ORIGINAL_FILENAME", binary),
    310        productname=overrides.get("WIN32_MODULE_PRODUCTNAME", display_name),
    311        buildid=buildid,
    312    )
    313 
    314    manifest_id = "2" if binary.lower().endswith(".dll") else "1"
    315    if binary and not has_manifest(data, manifest_id):
    316        manifest_path = os.path.join(srcdir, binary + ".manifest")
    317        if os.path.exists(manifest_path):
    318            manifest_path = manifest_path.replace("\\", "\\\\")
    319            data += f'\n{manifest_id} RT_MANIFEST "{manifest_path}"\n'
    320            extra_deps.add(manifest_path)
    321 
    322    target = binary or "module"
    323    with open(f"{target}.rc", "w", encoding="latin1") as fh:
    324        fh.write(data)
    325 
    326    if dep_file is not None and extra_deps:
    327        dep_dirname = os.path.dirname(dep_file)
    328        os.makedirs(dep_dirname, exist_ok=True)
    329 
    330        mk = Makefile()
    331        rule = mk.create_rule([target, f"{target}.rc"])
    332        rule.add_dependencies(sorted(extra_deps))
    333        with open(dep_file, "w") as dep_fd:
    334            mk.dump(dep_fd)
    335 
    336 
    337 if __name__ == "__main__":
    338    generate_module_rc()