def test_adds(self): m = InstallManifest() m.add_symlink('s_source', 's_dest') m.add_copy('c_source', 'c_dest') m.add_required_exists('e_dest') m.add_optional_exists('o_dest') m.add_pattern_symlink('ps_base', 'ps/*', 'ps_dest') m.add_pattern_copy('pc_base', 'pc/**', 'pc_dest') self.assertEqual(len(m), 6) self.assertIn('s_dest', m) self.assertIn('c_dest', m) self.assertIn('e_dest', m) self.assertIn('o_dest', m) with self.assertRaises(ValueError): m.add_symlink('s_other', 's_dest') with self.assertRaises(ValueError): m.add_copy('c_other', 'c_dest') with self.assertRaises(ValueError): m.add_required_exists('e_dest') with self.assertRaises(ValueError): m.add_optional_exists('o_dest') with self.assertRaises(ValueError): m.add_pattern_symlink('ps_base', 'ps/*', 'ps_dest') with self.assertRaises(ValueError): m.add_pattern_copy('pc_base', 'pc/**', 'pc_dest')
def _manifest_for_project(self, srcdir, project): manifest = InstallManifest() if project.manifest: manifest.add_copy(mozpath.join(srcdir, project.manifest), 'AndroidManifest.xml') if project.res: manifest.add_symlink(mozpath.join(srcdir, project.res), 'res') else: # Eclipse expects a res directory no matter what, so we # make an empty directory if the project doesn't specify. res = os.path.abspath(mozpath.join(os.path.dirname(__file__), 'templates', 'android_eclipse_empty_resource_directory')) manifest.add_pattern_copy(res, '.**', 'res') if project.assets: manifest.add_symlink(mozpath.join(srcdir, project.assets), 'assets') for cpe in project._classpathentries: manifest.add_symlink(mozpath.join(srcdir, cpe.srcdir), cpe.dstdir) # JARs and native libraries go in the same place. For now, # we're adding class path entries with the full path to # required JAR files (which makes sense for JARs in the source # directory, but probably doesn't for JARs in the object # directory). This could be a problem because we only know # the contents of (a subdirectory of) libs/ after a successful # build and package, which is after build-backend time. So we # use a pattern symlink that is resolved at manifest install # time. if project.libs: manifest.add_pattern_copy(mozpath.join(srcdir, project.libs), '**', 'libs') return manifest
def _manifest_for_project(self, srcdir, project): manifest = InstallManifest() if project.manifest: manifest.add_copy(mozpath.join(srcdir, project.manifest), 'AndroidManifest.xml') if project.res: manifest.add_symlink(mozpath.join(srcdir, project.res), 'res') else: # Eclipse expects a res directory no matter what, so we # make an empty directory if the project doesn't specify. res = os.path.abspath(mozpath.join(os.path.dirname(__file__), 'templates', 'android_eclipse_empty_resource_directory')) manifest.add_pattern_copy(res, '.**', 'res') if project.assets: manifest.add_symlink(mozpath.join(srcdir, project.assets), 'assets') for cpe in project._classpathentries: manifest.add_symlink(mozpath.join(srcdir, cpe.srcdir), cpe.dstdir) # JARs and native libraries go in the same place. For now, we're adding # class path entries with the full path to required JAR files (which # makes sense for JARs in the source directory, but probably doesn't for # JARs in the object directory). This could be a problem because we only # know the contents of (a subdirectory of) libs/ after a successful # build and package, which is after build-backend time. At the cost of # some flexibility, we explicitly copy certain libraries here; if the # libraries aren't present -- namely, when the tree hasn't been packaged # -- this fails. That's by design, to avoid crashes on device caused by # missing native libraries. for src, dst in project.libs: manifest.add_copy(mozpath.join(srcdir, src), dst) return manifest
def _manifest_for_project(self, srcdir, project): manifest = InstallManifest() if project.manifest: manifest.add_copy(mozpath.join(srcdir, project.manifest), "AndroidManifest.xml") if project.res: manifest.add_symlink(mozpath.join(srcdir, project.res), "res") if project.assets: manifest.add_symlink(mozpath.join(srcdir, project.assets), "assets") for cpe in project._classpathentries: manifest.add_symlink(mozpath.join(srcdir, cpe.srcdir), cpe.dstdir) # JARs and native libraries go in the same place. This # wouldn't be a problem, except we only know the contents of # (a subdirectory of) libs/ after a successful build and # package, which is after build-backend time. So we use a # pattern symlink that is resolved at manifest install time. if project.libs: manifest.add_pattern_copy(mozpath.join(srcdir, project.libs), "**", "libs") for extra_jar in sorted(project.extra_jars): manifest.add_copy(mozpath.join(srcdir, extra_jar), mozpath.join("libs", os.path.basename(extra_jar))) return manifest
def _manifest_for_project(self, srcdir, project): manifest = InstallManifest() if project.manifest: manifest.add_copy(mozpath.join(srcdir, project.manifest), 'AndroidManifest.xml') if project.res: manifest.add_symlink(mozpath.join(srcdir, project.res), 'res') if project.assets: manifest.add_symlink(mozpath.join(srcdir, project.assets), 'assets') for cpe in project._classpathentries: manifest.add_symlink(mozpath.join(srcdir, cpe.srcdir), cpe.dstdir) # JARs and native libraries go in the same place. For now, # we're adding class path entries with the full path to # required JAR files (which makes sense for JARs in the source # directory, but probably doesn't for JARs in the object # directory). This could be a problem because we only know # the contents of (a subdirectory of) libs/ after a successful # build and package, which is after build-backend time. So we # use a pattern symlink that is resolved at manifest install # time. if project.libs: manifest.add_pattern_copy(mozpath.join(srcdir, project.libs), '**', 'libs') return manifest
def _get_test_manifest(self): m = InstallManifest() m.add_symlink(self.tmppath('s_source'), 's_dest') m.add_copy(self.tmppath('c_source'), 'c_dest') m.add_required_exists('e_dest') m.add_optional_exists('o_dest') m.add_pattern_symlink('ps_base', '*', 'ps_dest') m.add_pattern_copy('pc_base', '**', 'pc_dest') return m
def _get_test_manifest(self): m = InstallManifest() m.add_symlink(self.tmppath('s_source'), 's_dest') m.add_copy(self.tmppath('c_source'), 'c_dest') m.add_preprocess(self.tmppath('p_source'), 'p_dest', self.tmppath('p_source.pp'), '#', {'FOO':'BAR', 'BAZ':'QUX'}) m.add_required_exists('e_dest') m.add_optional_exists('o_dest') m.add_pattern_symlink('ps_base', '*', 'ps_dest') m.add_pattern_copy('pc_base', '**', 'pc_dest') return m
def _get_test_manifest(self): m = InstallManifest() m.add_link(self.tmppath('s_source'), 's_dest') m.add_copy(self.tmppath('c_source'), 'c_dest') m.add_preprocess(self.tmppath('p_source'), 'p_dest', self.tmppath('p_source.pp'), '#', {'FOO':'BAR', 'BAZ':'QUX'}) m.add_required_exists('e_dest') m.add_optional_exists('o_dest') m.add_pattern_link('ps_base', '*', 'ps_dest') m.add_pattern_copy('pc_base', '**', 'pc_dest') m.add_content('the content\non\nmultiple lines', 'content') return m
def _get_test_manifest(self): m = InstallManifest() m.add_symlink(self.tmppath("s_source"), "s_dest") m.add_copy(self.tmppath("c_source"), "c_dest") m.add_preprocess( self.tmppath("p_source"), "p_dest", self.tmppath("p_source.pp"), "#", {"FOO": "BAR", "BAZ": "QUX"} ) m.add_required_exists("e_dest") m.add_optional_exists("o_dest") m.add_pattern_symlink("ps_base", "*", "ps_dest") m.add_pattern_copy("pc_base", "**", "pc_dest") return m
def test_adds(self): m = InstallManifest() m.add_link("s_source", "s_dest") m.add_copy("c_source", "c_dest") m.add_required_exists("e_dest") m.add_optional_exists("o_dest") m.add_pattern_link("ps_base", "ps/*", "ps_dest") m.add_pattern_copy("pc_base", "pc/**", "pc_dest") m.add_preprocess("p_source", "p_dest", "p_source.pp") m.add_content("content", "content") self.assertEqual(len(m), 8) self.assertIn("s_dest", m) self.assertIn("c_dest", m) self.assertIn("p_dest", m) self.assertIn("e_dest", m) self.assertIn("o_dest", m) self.assertIn("content", m) with self.assertRaises(ValueError): m.add_link("s_other", "s_dest") with self.assertRaises(ValueError): m.add_copy("c_other", "c_dest") with self.assertRaises(ValueError): m.add_preprocess("p_other", "p_dest", "p_other.pp") with self.assertRaises(ValueError): m.add_required_exists("e_dest") with self.assertRaises(ValueError): m.add_optional_exists("o_dest") with self.assertRaises(ValueError): m.add_pattern_link("ps_base", "ps/*", "ps_dest") with self.assertRaises(ValueError): m.add_pattern_copy("pc_base", "pc/**", "pc_dest") with self.assertRaises(ValueError): m.add_content("content", "content")
def _get_test_manifest(self): m = InstallManifest() m.add_link(self.tmppath("s_source"), "s_dest") m.add_copy(self.tmppath("c_source"), "c_dest") m.add_preprocess( self.tmppath("p_source"), "p_dest", self.tmppath("p_source.pp"), "#", { "FOO": "BAR", "BAZ": "QUX" }, ) m.add_required_exists("e_dest") m.add_optional_exists("o_dest") m.add_pattern_link("ps_base", "*", "ps_dest") m.add_pattern_copy("pc_base", "**", "pc_dest") m.add_content("the content\non\nmultiple lines", "content") return m
def test_adds(self): m = InstallManifest() m.add_symlink("s_source", "s_dest") m.add_copy("c_source", "c_dest") m.add_required_exists("e_dest") m.add_optional_exists("o_dest") m.add_pattern_symlink("ps_base", "ps/*", "ps_dest") m.add_pattern_copy("pc_base", "pc/**", "pc_dest") m.add_preprocess("p_source", "p_dest", "p_source.pp") self.assertEqual(len(m), 7) self.assertIn("s_dest", m) self.assertIn("c_dest", m) self.assertIn("p_dest", m) self.assertIn("e_dest", m) self.assertIn("o_dest", m) with self.assertRaises(ValueError): m.add_symlink("s_other", "s_dest") with self.assertRaises(ValueError): m.add_copy("c_other", "c_dest") with self.assertRaises(ValueError): m.add_preprocess("p_other", "p_dest", "p_other.pp") with self.assertRaises(ValueError): m.add_required_exists("e_dest") with self.assertRaises(ValueError): m.add_optional_exists("o_dest") with self.assertRaises(ValueError): m.add_pattern_symlink("ps_base", "ps/*", "ps_dest") with self.assertRaises(ValueError): m.add_pattern_copy("pc_base", "pc/**", "pc_dest")
def repackage_msix( dir_or_package, channel=None, branding=None, template=None, distribution_dirs=[], locale_allowlist=set(), version=None, vendor=None, displayname=None, app_name="firefox", identity=None, publisher=None, publisher_display_name="Mozilla Corporation", arch=None, output=None, force=False, log=None, verbose=False, makeappx=None, ): if not channel: raise Exception("channel is required") if channel not in ["official", "beta", "aurora", "nightly", "unofficial"]: raise Exception("channel is unrecognized: {}".format(channel)) if not branding: raise Exception("branding dir is required") if not os.path.isdir(branding): raise Exception("branding dir {} does not exist".format(branding)) # TODO: maybe we can fish this from the package directly? Maybe from a DLL, # maybe from application.ini? if arch is None or arch not in _MSIX_ARCH.keys(): raise Exception( "arch name must be provided and one of {}.".format(_MSIX_ARCH.keys()) ) if not os.path.exists(dir_or_package): raise Exception("{} does not exist".format(dir_or_package)) if ( os.path.isfile(dir_or_package) and os.path.splitext(dir_or_package)[1] == ".msix" ): # The convention is $MOZBUILD_STATE_PATH/cache/$FEATURE. msix_dir = mozpath.normsep( mozpath.join( get_state_dir(), "cache", "mach-msix", "msix-unpack", ) ) if os.path.exists(msix_dir): shutil.rmtree(msix_dir) ensureParentDir(msix_dir) dir_or_package = unpack_msix(dir_or_package, msix_dir, log=log, verbose=verbose) log( logging.INFO, "msix", { "input": dir_or_package, }, "Adding files from '{input}'", ) if os.path.isdir(dir_or_package): finder = FileFinder(dir_or_package) else: finder = JarFinder(dir_or_package, JarReader(dir_or_package)) values = get_application_ini_values( finder, dict(section="App", value="CodeName", fallback="Name"), dict(section="App", value="Vendor"), ) first = next(values) displayname = displayname or "Mozilla {}".format(first) second = next(values) vendor = vendor or second # For `AppConstants.jsm` and `brand.properties`, which are in the omnijar in packaged builds. # The nested langpack XPI files can't be read by `mozjar.py`. unpack_finder = UnpackFinder(finder, unpack_xpi=False) if not version: values = get_appconstants_jsm_values( unpack_finder, "MOZ_APP_VERSION_DISPLAY", "MOZ_BUILDID" ) display_version = next(values) buildid = next(values) version = get_embedded_version(display_version, buildid) log( logging.INFO, "msix", { "version": version, "display_version": display_version, "buildid": buildid, }, "AppConstants.jsm display version is '{display_version}' and build ID is '{buildid}':" + " embedded version will be '{version}'", ) # TODO: Bug 1721922: localize this description via Fluent. lines = [] for _, f in unpack_finder.find("**/chrome/en-US/locale/branding/brand.properties"): lines.extend( line for line in f.open().read().decode("utf-8").splitlines() if "brandFullName" in line ) (brandFullName,) = lines # We expect exactly one definition. _, _, brandFullName = brandFullName.partition("=") brandFullName = brandFullName.strip() # We don't have a build at repackage-time to gives us this value, and the # source of truth is a branding-specific `configure.sh` shell script that we # can't easily evaluate completely here. Instead, we take the last value # from `configure.sh`. lines = [ line for line in open(mozpath.join(branding, "configure.sh")).readlines() if "MOZ_IGECKOBACKCHANNEL_IID" in line ] MOZ_IGECKOBACKCHANNEL_IID = lines[-1] _, _, MOZ_IGECKOBACKCHANNEL_IID = MOZ_IGECKOBACKCHANNEL_IID.partition("=") MOZ_IGECKOBACKCHANNEL_IID = MOZ_IGECKOBACKCHANNEL_IID.strip() if MOZ_IGECKOBACKCHANNEL_IID.startswith(('"', "'")): MOZ_IGECKOBACKCHANNEL_IID = MOZ_IGECKOBACKCHANNEL_IID[1:-1] # The convention is $MOZBUILD_STATE_PATH/cache/$FEATURE. output_dir = mozpath.normsep( mozpath.join( get_state_dir(), "cache", "mach-msix", "msix-temp-{}".format(channel) ) ) if channel == "beta": # Release (official) and Beta share branding. Differentiate Beta a little bit. displayname += " Beta" brandFullName += " Beta" # Like 'Firefox Package Root', 'Firefox Nightly Package Root', 'Firefox Beta # Package Root'. This is `BrandFullName` in the installer, and we want to # be close but to not match. By not matching, we hope to prevent confusion # and/or errors between regularly installed builds and App Package builds. instdir = "{} Package Root".format(displayname) # The standard package name is like "CompanyNoSpaces.ProductNoSpaces". identity = identity or "{}.{}".format(vendor, displayname).replace(" ", "") # We might want to include the publisher ID hash here. I.e., # "__{publisherID}". My locally produced MSIX was named like # `Mozilla.MozillaFirefoxNightly_89.0.0.0_x64__4gf61r4q480j0`, suggesting also a # missing field, but it's not necessary, since this is just an output file name. package_output_name = "{identity}_{version}_{arch}".format( identity=identity, version=version, arch=_MSIX_ARCH[arch] ) # The convention is $MOZBUILD_STATE_PATH/cache/$FEATURE. default_output = mozpath.normsep( mozpath.join( get_state_dir(), "cache", "mach-msix", "{}.msix".format(package_output_name) ) ) output = output or default_output log(logging.INFO, "msix", {"output": output}, "Repackaging to: {output}") m = InstallManifest() m.add_copy(mozpath.join(template, "Resources.pri"), "Resources.pri") m.add_pattern_copy(mozpath.join(branding, "msix", "Assets"), "**", "Assets") m.add_pattern_copy(mozpath.join(template, "VFS"), "**", "VFS") copier = FileCopier() # TODO: Bug 1710147: filter out MSVCRT files and use a dependency instead. for p, f in finder: if not os.path.isdir(dir_or_package): # In archived builds, `p` is like "firefox/firefox.exe"; we want just "firefox.exe". pp = os.path.relpath(p, "firefox") else: # In local builds and unpacked MSIX directories, `p` is like "firefox.exe" already. pp = p if pp.startswith("distribution"): # Treat any existing distribution as a distribution directory, # potentially with language packs. This makes it easy to repack # unpacked MSIXes. distribution_dir = mozpath.join(dir_or_package, "distribution") if distribution_dir not in distribution_dirs: distribution_dirs.append(distribution_dir) continue copier.add(mozpath.normsep(mozpath.join("VFS", "ProgramFiles", instdir, pp)), f) # Locales to declare as supported in `AppxManifest.xml`. locales = set(["en-US"]) for distribution_dir in [ mozpath.join(template, "distribution") ] + distribution_dirs: log( logging.INFO, "msix", {"dir": distribution_dir}, "Adding distribution files from {dir}", ) # In automation, we have no easy way to remap the names of artifacts fetched from dependent # tasks. In particular, langpacks will be named like `target.langpack.xpi`. The fetch # tasks do allow us to put them in a per-locale directory, so that the entire set can be # fetched. Here we remap the names. finder = FileFinder(distribution_dir) for p, f in finder: locale = None if os.path.basename(p) == "target.langpack.xpi": # Turn "/path/to/LOCALE/target.langpack.xpi" into "LOCALE". This is how langpacks # are presented in CI. base, locale = os.path.split(os.path.dirname(p)) # Like "locale-LOCALE/[email protected]". This is what AMO # serves and how flatpak builds name langpacks, but not how snap builds name # langpacks. I can't explain the discrepancy. dest = mozpath.normsep( mozpath.join( base, f"locale-{locale}", f"langpack-{locale}@firefox.mozilla.org.xpi", ) ) log( logging.DEBUG, "msix", {"path": p, "dest": dest}, "Renaming langpack {path} to {dest}", ) elif os.path.basename(p).startswith("langpack-"): # Turn "/path/to/[email protected]" into "LOCALE". This is # how langpacks are presented from an unpacked MSIX. _, _, locale = os.path.basename(p).partition("langpack-") locale, _, _ = locale.partition("@") dest = p else: dest = p if locale: locale = locale.strip().lower() locales.add(locale) log( logging.DEBUG, "msix", {"locale": locale, "dest": dest}, "Distributing locale '{locale}' from {dest}", ) dest = mozpath.normsep( mozpath.join("VFS", "ProgramFiles", instdir, "distribution", dest) ) if copier.contains(dest): log( logging.INFO, "msix", {"dest": dest, "path": mozpath.join(finder.base, p)}, "Skipping duplicate: {dest} from {path}", ) continue log( logging.DEBUG, "msix", {"dest": dest, "path": mozpath.join(finder.base, p)}, "Adding distribution path: {dest} from {path}", ) copier.add( dest, f, ) locales.remove("en-US") # Windows MSIX packages support a finite set of locales: see # https://docs.microsoft.com/en-us/windows/uwp/publish/supported-languages, which is encoded in # https://searchfox.org/mozilla-central/source/browser/installer/windows/msix/msix-all-locales. # We distribute all of the langpacks supported by the release channel in our MSIX, which is # encoded in https://searchfox.org/mozilla-central/source/browser/locales/all-locales. But we # only advertise support in the App manifest for the intersection of that set and the set of # supported locales. # # We distribute all langpacks to avoid the following issue. Suppose a user manually installs a # langpack that is not supported by Windows, and then updates the installed MSIX package. MSIX # package upgrades are essentially paveover installs, so there is no opportunity for Firefox to # update the langpack before the update. But, since all langpacks are bundled with the MSIX, # that langpack will be up-to-date, preventing one class of YSOD. unadvertised = set() if locale_allowlist: unadvertised = locales - locale_allowlist locales = locales & locale_allowlist for locale in sorted(unadvertised): log( logging.INFO, "msix", {"locale": locale}, "Not advertising distributed locale '{locale}' that is not recognized by Windows", ) locales = ["en-US"] + list(sorted(locales)) resource_language_list = "\n".join( f' <Resource Language="{locale}" />' for locale in sorted(locales) ) defines = { "APPX_ARCH": _MSIX_ARCH[arch], "APPX_DISPLAYNAME": brandFullName, "APPX_DESCRIPTION": brandFullName, # Like 'Mozilla.MozillaFirefox', 'Mozilla.MozillaFirefoxBeta', or # 'Mozilla.MozillaFirefoxNightly'. "APPX_IDENTITY": identity, # Like 'Firefox Package Root', 'Firefox Nightly Package Root', 'Firefox # Beta Package Root'. See above. "APPX_INSTDIR": instdir, # Like 'Firefox%20Package%20Root'. "APPX_INSTDIR_QUOTED": urllib.parse.quote(instdir), "APPX_PUBLISHER": publisher, "APPX_PUBLISHER_DISPLAY_NAME": publisher_display_name, "APPX_RESOURCE_LANGUAGE_LIST": resource_language_list, "APPX_VERSION": version, "MOZ_APP_DISPLAYNAME": displayname, "MOZ_APP_NAME": app_name, "MOZ_IGECKOBACKCHANNEL_IID": MOZ_IGECKOBACKCHANNEL_IID, } m.add_preprocess( mozpath.join(template, "AppxManifest.xml.in"), "AppxManifest.xml", [], defines=defines, marker="<!-- #", # So that we can have well-formed XML. ) m.populate_registry(copier) output_dir = mozpath.abspath(output_dir) ensureParentDir(output_dir) start = time.time() result = copier.copy( output_dir, remove_empty_directories=True, skip_if_older=not force ) if log: log_copy_result(log, time.time() - start, output_dir, result) if verbose: # Dump AppxManifest.xml contents for ease of debugging. log(logging.DEBUG, "msix", {}, "AppxManifest.xml") log(logging.DEBUG, "msix", {}, ">>>") for line in open(mozpath.join(output_dir, "AppxManifest.xml")).readlines(): log(logging.DEBUG, "msix", {}, line[:-1]) # Drop trailing line terminator. log(logging.DEBUG, "msix", {}, "<<<") if not makeappx: makeappx = find_sdk_tool("makeappx.exe", log=log) if not makeappx: raise ValueError( "makeappx is required; " "set MAKEAPPX or WINDOWSSDKDIR or PATH" ) # `makeappx.exe` supports both slash and hyphen style arguments; `makemsix` # supports only hyphen style. `makeappx.exe` allows to overwrite and to # provide more feedback, so we prefer invoking with these flags. This will # also accommodate `wine makeappx.exe`. stdout = subprocess.run( [makeappx], check=False, capture_output=True, universal_newlines=True ).stdout is_makeappx = "MakeAppx Tool" in stdout if is_makeappx: args = [makeappx, "pack", "/d", output_dir, "/p", output, "/overwrite"] else: args = [makeappx, "pack", "-d", output_dir, "-p", output] if verbose and is_makeappx: args.append("/verbose") joined = " ".join(shlex_quote(arg) for arg in args) log(logging.INFO, "msix", {"args": args, "joined": joined}, "Invoking: {joined}") sys.stdout.flush() # Otherwise the subprocess output can be interleaved. if verbose: subprocess.check_call(args, universal_newlines=True) else: # Suppress output unless we fail. try: subprocess.check_output(args, universal_newlines=True) except subprocess.CalledProcessError as e: sys.stderr.write(e.output) raise return output