summaryrefslogtreecommitdiffstats
path: root/python/mozperftest/mozperftest/system/macos.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/mozperftest/mozperftest/system/macos.py')
-rw-r--r--python/mozperftest/mozperftest/system/macos.py120
1 files changed, 120 insertions, 0 deletions
diff --git a/python/mozperftest/mozperftest/system/macos.py b/python/mozperftest/mozperftest/system/macos.py
new file mode 100644
index 0000000000..493fd4fc1d
--- /dev/null
+++ b/python/mozperftest/mozperftest/system/macos.py
@@ -0,0 +1,120 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import os
+import platform
+import shutil
+import subprocess
+import tempfile
+from pathlib import Path
+
+from mozperftest.layers import Layer
+
+# Add here any option that might point to a DMG file we want to extract. The key
+# is name of the option and the value, the file in the DMG we want to use for
+# the option.
+POTENTIAL_DMGS = {
+ "browsertime-binary": "Contents/MacOS/firefox",
+ "xpcshell-xre-path": "Contents/MacOS",
+}
+
+
+class MacosDevice(Layer):
+ """Runs on macOS to mount DMGs if we see one."""
+
+ name = "macos"
+ activated = platform.system() == "Darwin"
+
+ def __init__(self, env, mach_cmd):
+ super(MacosDevice, self).__init__(env, mach_cmd)
+ self._tmp_dirs = []
+
+ def _run_process(self, args):
+ p = subprocess.Popen(
+ args,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ universal_newlines=True,
+ )
+
+ stdout, stderr = p.communicate(timeout=45)
+ if p.returncode != 0:
+ raise subprocess.CalledProcessError(
+ stdout=stdout, stderr=stderr, returncode=p.returncode
+ )
+
+ return stdout
+
+ def extract_app(self, dmg, target):
+ mount = Path(tempfile.mkdtemp())
+
+ if not Path(dmg).exists():
+ raise FileNotFoundError(dmg)
+
+ # mounting the DMG with hdiutil
+ cmd = f"hdiutil attach -nobrowse -mountpoint {str(mount)} {dmg}"
+ try:
+ self._run_process(cmd.split())
+ except subprocess.CalledProcessError:
+ self.error(f"Can't mount {dmg}")
+ if mount.exists():
+ shutil.rmtree(str(mount))
+ raise
+
+ # browse the mounted volume, to look for the app.
+ found = False
+ try:
+ for f in os.listdir(str(mount)):
+ if not f.endswith(".app"):
+ continue
+ app = mount / f
+ shutil.copytree(str(app), str(target))
+ found = True
+ break
+ finally:
+ try:
+ self._run_process(f"hdiutil detach {str(mount)}".split())
+ except subprocess.CalledProcessError as e: # noqa
+ self.warning("Detach failed {e.stdout}")
+ finally:
+ if mount.exists():
+ shutil.rmtree(str(mount))
+ if not found:
+ self.error(f"No app file found in {dmg}")
+ raise IOError(dmg)
+
+ def run(self, metadata):
+ # Each DMG is mounted, then we look for the .app
+ # directory in it, which is copied in a directory
+ # alongside the .dmg file. That directory
+ # is removed during teardown.
+ for option, path_in_dmg in POTENTIAL_DMGS.items():
+ value = self.get_arg(option)
+
+ if value is None or not value.endswith(".dmg"):
+ continue
+
+ self.info(f"Mounting {value}")
+ dmg_file = Path(value)
+ if not dmg_file.exists():
+ raise FileNotFoundError(str(dmg_file))
+
+ # let's unpack the DMG in place...
+ target = dmg_file.parent / dmg_file.name.split(".")[0]
+ self._tmp_dirs.append(target)
+ self.extract_app(dmg_file, target)
+
+ # ... find a specific file or directory if needed ...
+ path = target / path_in_dmg
+ if not path.exists():
+ raise FileNotFoundError(str(path))
+
+ # ... and swap the browsertime argument
+ self.info(f"Using {path} for {option}")
+ self.env.set_arg(option, str(path))
+ return metadata
+
+ def teardown(self):
+ for dir in self._tmp_dirs:
+ if dir.exists():
+ shutil.rmtree(str(dir))