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()