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_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') 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_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') with self.assertRaises(ValueError): m.add_content('content', 'content')
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') 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_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') with self.assertRaises(ValueError): m.add_content('content', 'content')
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 test_preprocessor_dependencies(self): manifest = self.tmppath('m') deps = self.tmppath('m.pp') dest = self.tmppath('dest') source = self.tmppath('p_source') destfile = self.tmppath('dest/p_dest') include = self.tmppath('p_incl') os.mkdir(dest) with open(source, 'wt') as fh: fh.write('#define SRC\nSOURCE\n') time = os.path.getmtime(source) - 3 os.utime(source, (time, time)) with open(include, 'wt') as fh: fh.write('INCLUDE\n') time = os.path.getmtime(source) - 3 os.utime(include, (time, time)) # Create and write a manifest with the preprocessed file. m = InstallManifest() m.add_preprocess(source, 'p_dest', deps, '#', { 'FOO': 'BAR', 'BAZ': 'QUX' }) m.write(path=manifest) time = os.path.getmtime(source) - 5 os.utime(manifest, (time, time)) # Now read the manifest back in, and apply it. This should write out # our preprocessed file. m = InstallManifest(path=manifest) c = FileCopier() m.populate_registry(c) self.assertTrue(c.copy(dest)) with open(destfile, 'rt') as fh: self.assertEqual(fh.read(), 'SOURCE\n') # Next, modify the source to #INCLUDE another file. with open(source, 'wt') as fh: fh.write('SOURCE\n#include p_incl\n') time = os.path.getmtime(source) - 1 os.utime(destfile, (time, time)) # Apply the manifest, and confirm that it also reads the newly included # file. m = InstallManifest(path=manifest) c = FileCopier() m.populate_registry(c) c.copy(dest) with open(destfile, 'rt') as fh: self.assertEqual(fh.read(), 'SOURCE\nINCLUDE\n') # Set the time on the source file back, so it won't be picked up as # modified in the next test. time = os.path.getmtime(source) - 1 os.utime(source, (time, time)) # Now, modify the include file (but not the original source). with open(include, 'wt') as fh: fh.write('INCLUDE MODIFIED\n') time = os.path.getmtime(include) - 1 os.utime(destfile, (time, time)) # Apply the manifest, and confirm that the change to the include file # is detected. That should cause the preprocessor to run again. m = InstallManifest(path=manifest) c = FileCopier() m.populate_registry(c) c.copy(dest) with open(destfile, 'rt') as fh: self.assertEqual(fh.read(), 'SOURCE\nINCLUDE MODIFIED\n') # ORing an InstallManifest should copy file dependencies m = InstallManifest() m |= InstallManifest(path=manifest) c = FileCopier() m.populate_registry(c) e = c._files['p_dest'] self.assertEqual(e.extra_depends, [manifest])
def test_preprocessor(self): manifest = self.tmppath('m') deps = self.tmppath('m.pp') dest = self.tmppath('dest') include = self.tmppath('p_incl') with open(include, 'wt') as fh: fh.write('#define INCL\n') time = os.path.getmtime(include) - 3 os.utime(include, (time, time)) with open(self.tmppath('p_source'), 'wt') as fh: fh.write('#ifdef FOO\n#if BAZ == QUX\nPASS1\n#endif\n#endif\n') fh.write('#ifdef DEPTEST\nPASS2\n#endif\n') fh.write('#include p_incl\n#ifdef INCLTEST\nPASS3\n#endif\n') time = os.path.getmtime(self.tmppath('p_source')) - 3 os.utime(self.tmppath('p_source'), (time, time)) # Create and write a manifest with the preprocessed file, then apply it. # This should write out our preprocessed file. m = InstallManifest() m.add_preprocess(self.tmppath('p_source'), 'p_dest', deps, '#', { 'FOO': 'BAR', 'BAZ': 'QUX' }) m.write(path=manifest) m = InstallManifest(path=manifest) c = FileCopier() m.populate_registry(c) c.copy(dest) self.assertTrue(os.path.exists(self.tmppath('dest/p_dest'))) with open(self.tmppath('dest/p_dest'), 'rt') as fh: self.assertEqual(fh.read(), 'PASS1\n') # Create a second manifest with the preprocessed file, then apply it. # Since this manifest does not exist on the disk, there should not be a # dependency on it, and the preprocessed file should not be modified. m2 = InstallManifest() m2.add_preprocess(self.tmppath('p_source'), 'p_dest', deps, '#', {'DEPTEST': True}) c = FileCopier() m2.populate_registry(c) result = c.copy(dest) self.assertFalse(self.tmppath('dest/p_dest') in result.updated_files) self.assertTrue(self.tmppath('dest/p_dest') in result.existing_files) # Write out the second manifest, then load it back in from the disk. # This should add the dependency on the manifest file, so our # preprocessed file should be regenerated with the new defines. # We also set the mtime on the destination file back, so it will be # older than the manifest file. m2.write(path=manifest) time = os.path.getmtime(manifest) - 1 os.utime(self.tmppath('dest/p_dest'), (time, time)) m2 = InstallManifest(path=manifest) c = FileCopier() m2.populate_registry(c) self.assertTrue(c.copy(dest)) with open(self.tmppath('dest/p_dest'), 'rt') as fh: self.assertEqual(fh.read(), 'PASS2\n') # Set the time on the manifest back, so it won't be picked up as # modified in the next test time = os.path.getmtime(manifest) - 1 os.utime(manifest, (time, time)) # Update the contents of a file included by the source file. This should # cause the destination to be regenerated. with open(include, 'wt') as fh: fh.write('#define INCLTEST\n') time = os.path.getmtime(include) - 1 os.utime(self.tmppath('dest/p_dest'), (time, time)) c = FileCopier() m2.populate_registry(c) self.assertTrue(c.copy(dest)) with open(self.tmppath('dest/p_dest'), 'rt') as fh: self.assertEqual(fh.read(), 'PASS2\nPASS3\n')
def test_preprocessor_dependencies(self): manifest = self.tmppath('m') deps = self.tmppath('m.pp') dest = self.tmppath('dest') source = self.tmppath('p_source') destfile = self.tmppath('dest/p_dest') include = self.tmppath('p_incl') os.mkdir(dest) with open(source, 'wt') as fh: fh.write('#define SRC\nSOURCE\n') time = os.path.getmtime(source) - 3 os.utime(source, (time, time)) with open(include, 'wt') as fh: fh.write('INCLUDE\n') time = os.path.getmtime(source) - 3 os.utime(include, (time, time)) # Create and write a manifest with the preprocessed file. m = InstallManifest() m.add_preprocess(source, 'p_dest', deps, '#', {'FOO':'BAR', 'BAZ':'QUX'}) m.write(path=manifest) time = os.path.getmtime(source) - 5 os.utime(manifest, (time, time)) # Now read the manifest back in, and apply it. This should write out # our preprocessed file. m = InstallManifest(path=manifest) c = FileCopier() m.populate_registry(c) self.assertTrue(c.copy(dest)) with open(destfile, 'rt') as fh: self.assertEqual(fh.read(), 'SOURCE\n') # Next, modify the source to #INCLUDE another file. with open(source, 'wt') as fh: fh.write('SOURCE\n#include p_incl\n') time = os.path.getmtime(source) - 1 os.utime(destfile, (time, time)) # Apply the manifest, and confirm that it also reads the newly included # file. m = InstallManifest(path=manifest) c = FileCopier() m.populate_registry(c) c.copy(dest) with open(destfile, 'rt') as fh: self.assertEqual(fh.read(), 'SOURCE\nINCLUDE\n') # Set the time on the source file back, so it won't be picked up as # modified in the next test. time = os.path.getmtime(source) - 1 os.utime(source, (time, time)) # Now, modify the include file (but not the original source). with open(include, 'wt') as fh: fh.write('INCLUDE MODIFIED\n') time = os.path.getmtime(include) - 1 os.utime(destfile, (time, time)) # Apply the manifest, and confirm that the change to the include file # is detected. That should cause the preprocessor to run again. m = InstallManifest(path=manifest) c = FileCopier() m.populate_registry(c) c.copy(dest) with open(destfile, 'rt') as fh: self.assertEqual(fh.read(), 'SOURCE\nINCLUDE MODIFIED\n') # ORing an InstallManifest should copy file dependencies m = InstallManifest() m |= InstallManifest(path=manifest) c = FileCopier() m.populate_registry(c) e = c._files['p_dest'] self.assertEqual(e.extra_depends, [manifest])
def test_preprocessor(self): manifest = self.tmppath('m') deps = self.tmppath('m.pp') dest = self.tmppath('dest') include = self.tmppath('p_incl') with open(include, 'wt') as fh: fh.write('#define INCL\n') time = os.path.getmtime(include) - 3 os.utime(include, (time, time)) with open(self.tmppath('p_source'), 'wt') as fh: fh.write('#ifdef FOO\n#if BAZ == QUX\nPASS1\n#endif\n#endif\n') fh.write('#ifdef DEPTEST\nPASS2\n#endif\n') fh.write('#include p_incl\n#ifdef INCLTEST\nPASS3\n#endif\n') time = os.path.getmtime(self.tmppath('p_source')) - 3 os.utime(self.tmppath('p_source'), (time, time)) # Create and write a manifest with the preprocessed file, then apply it. # This should write out our preprocessed file. m = InstallManifest() m.add_preprocess(self.tmppath('p_source'), 'p_dest', deps, '#', {'FOO':'BAR', 'BAZ':'QUX'}) m.write(path=manifest) m = InstallManifest(path=manifest) c = FileCopier() m.populate_registry(c) c.copy(dest) self.assertTrue(os.path.exists(self.tmppath('dest/p_dest'))) with open(self.tmppath('dest/p_dest'), 'rt') as fh: self.assertEqual(fh.read(), 'PASS1\n') # Create a second manifest with the preprocessed file, then apply it. # Since this manifest does not exist on the disk, there should not be a # dependency on it, and the preprocessed file should not be modified. m2 = InstallManifest() m2.add_preprocess(self.tmppath('p_source'), 'p_dest', deps, '#', {'DEPTEST':True}) c = FileCopier() m2.populate_registry(c) result = c.copy(dest) self.assertFalse(self.tmppath('dest/p_dest') in result.updated_files) self.assertTrue(self.tmppath('dest/p_dest') in result.existing_files) # Write out the second manifest, then load it back in from the disk. # This should add the dependency on the manifest file, so our # preprocessed file should be regenerated with the new defines. # We also set the mtime on the destination file back, so it will be # older than the manifest file. m2.write(path=manifest) time = os.path.getmtime(manifest) - 1 os.utime(self.tmppath('dest/p_dest'), (time, time)) m2 = InstallManifest(path=manifest) c = FileCopier() m2.populate_registry(c) self.assertTrue(c.copy(dest)) with open(self.tmppath('dest/p_dest'), 'rt') as fh: self.assertEqual(fh.read(), 'PASS2\n') # Set the time on the manifest back, so it won't be picked up as # modified in the next test time = os.path.getmtime(manifest) - 1 os.utime(manifest, (time, time)) # Update the contents of a file included by the source file. This should # cause the destination to be regenerated. with open(include, 'wt') as fh: fh.write('#define INCLTEST\n') time = os.path.getmtime(include) - 1 os.utime(self.tmppath('dest/p_dest'), (time, time)) c = FileCopier() m2.populate_registry(c) self.assertTrue(c.copy(dest)) with open(self.tmppath('dest/p_dest'), 'rt') as fh: self.assertEqual(fh.read(), 'PASS2\nPASS3\n')
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