Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Linux #12

Merged
merged 7 commits into from
Dec 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions mixxx_bisect/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from mixxx_bisect.options import Options
from mixxx_bisect.runner import SnapshotRunner
from mixxx_bisect.runner.linux import LinuxSnapshotRunner
from mixxx_bisect.runner.macos import MacOSSnapshotRunner
from mixxx_bisect.runner.windows import WindowsSnapshotRunner
from mixxx_bisect.utils.git import clone_mixxx, commits_in_order, describe_commit, parse_commit, sort_commits
Expand All @@ -24,6 +25,7 @@
SNAPSHOT_RUNNERS: dict[str, type[SnapshotRunner]] = {
'Windows': WindowsSnapshotRunner,
'Darwin': MacOSSnapshotRunner,
'Linux': LinuxSnapshotRunner,
}

SNAPSHOT_REPOSITORIES: dict[str, type[SnapshotRepository]] = {
Expand All @@ -34,13 +36,15 @@
# Main

def main():
os = platform.system()

parser = argparse.ArgumentParser(description='Finds Mixxx regressions using binary search')
parser.add_argument('--repository', default='mixxx-org', choices=sorted(SNAPSHOT_REPOSITORIES.keys()), help=f'The snapshot repository to use.')
parser.add_argument('--branch', default='main', help=f'The branch to search for snapshots on, if supported by the hoster.')
parser.add_argument('--root', type=Path, default=DEFAULT_ROOT, help='The root directory where all application-specific state (i.e. the mixxx repo, downloads, mounted snapshots etc.) will be stored.')
parser.add_argument('--repository', default='m1xxx' if os == 'Linux' else 'mixxx-org', choices=sorted(SNAPSHOT_REPOSITORIES.keys()), help=f'The snapshot repository to use.')
parser.add_argument('--branch', default='main', help=f'The branch to search for snapshots on, if supported by the repository.')
parser.add_argument('--root', type=Path, default=DEFAULT_ROOT, help='The root directory where all application-specific state (i.e. the mixxx repo, downloads, installed snapshots etc.) will be stored.')
parser.add_argument('--dump-snapshots', action='store_true', help='Dumps the fetched snapshots.')
parser.add_argument('--verbose', action='store_true', help='Enables verbose output.')
parser.add_argument('--arch', default=platform.machine(), help="The architecture to query for. Defaults to `platform.machine()`, requires the hoster to provide corresponding binaries and is primarily useful for machines capable of running multiple architectures, e.g. via Rosetta or QEMU.")
parser.add_argument('--arch', default=platform.machine(), help="The architecture to query for. Defaults to `platform.machine()`, requires the repository to provide corresponding binaries and is primarily useful for machines capable of running multiple architectures, e.g. via Rosetta or QEMU.")
parser.add_argument('-v', '--version', action='store_true', help='Outputs the version.')
parser.add_argument('-q', '--quiet', action='store_true', help='Suppress output from subprocesses.')
parser.add_argument('-g', '--good', help='The lower bound of the commit range (a good commit)')
Expand All @@ -53,8 +57,6 @@ def main():
return

try:
os = platform.system()

if os not in SNAPSHOT_RUNNERS.keys():
raise UnsupportedOSError(f"Unsupported OS: {os} has no snapshot runner (supported are {', '.join(SNAPSHOT_RUNNERS.keys())})")

Expand All @@ -64,10 +66,11 @@ def main():
opts = Options(
quiet=args.quiet,
verbose=args.verbose,
os=os,
arch=args.arch,
root_dir=args.root,
mixxx_dir=args.root / 'mixxx.git',
mount_dir=args.root / 'mnt',
installs_dir=args.root / 'installs',
log_dir=args.root / 'log',
downloads_dir=args.root / 'downloads',
)
Expand All @@ -79,20 +82,20 @@ def main():
clone_mixxx(opts)

# Create auxiliary directories
for dir in [opts.downloads_dir, opts.mount_dir, opts.log_dir]:
for dir in [opts.downloads_dir, opts.installs_dir, opts.log_dir]:
dir.mkdir(parents=True, exist_ok=True)

# Set up platform-specific snapshot runner
runner = SnapshotRunner(opts)

# Fetch snapshots and match them up with Git commits
hoster = SnapshotRepository(
repository = SnapshotRepository(
branch=args.branch,
suffix=runner.download_path.suffix,
suffix=runner.suffix,
opts=opts
)

snapshots = hoster.fetch_snapshots()
snapshots = repository.fetch_snapshots()
if args.dump_snapshots:
print(json.dumps(snapshots, indent=2))

Expand Down
3 changes: 2 additions & 1 deletion mixxx_bisect/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
class Options:
quiet: bool
verbose: bool
os: str
arch: str
root_dir: Path
mixxx_dir: Path
mount_dir: Path
installs_dir: Path
log_dir: Path
downloads_dir: Path
24 changes: 16 additions & 8 deletions mixxx_bisect/repository/m1xxx.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from typing import Optional, cast
from mixxx_bisect.error import UnsupportedArchError
from mixxx_bisect.error import UnsupportedArchError, UnsupportedOSError

from mixxx_bisect.repository import SnapshotRepository
from mixxx_bisect.options import Options
Expand All @@ -23,16 +23,24 @@ def __init__(self, branch: str, suffix: str, opts: Options):
}.get(opts.arch)

if arch is None:
raise UnsupportedArchError(f'The architecture {opts.arch} is not supported by the m1xxx hoster.')
raise UnsupportedArchError(f'The architecture {opts.arch} is not supported by the m1xxx repository.')

os = {
'Darwin': 'osx',
'Linux': 'linux',
}.get(opts.os)

if os is None:
raise UnsupportedOSError(f'The os {opts.os} is not supported by the m1xxx repository.')

# TODO: Should we use the 'debugasserts' variant? Perhaps as an optional flag?
base_triplet_pattern = re.escape(arch) + r'-' + re.escape(os) + r'(?:-min\d+)?(?:-release)?'

self.snapshot_name_patterns = [
# Newest pattern, e.g. mixxx-2.5.0.c46027.r2c2e706b44-arm64-osx-min1100-release
# TODO: Should we use the 'debugasserts' variant? Perhaps as an optional flag?
re.compile(r'^mixxx-[\d\.]+\.c\d+\.r(\w+)-' + re.escape(arch) + r'-osx-min\d+-release$'),
# Newer pattern, e.g. mixxx-2.5.0.c45818.r30bca40dad-arm64-osx-min1100
re.compile(r'^mixxx-[\d\.]+\.c\d+\.r(\w+)-' + re.escape(arch) + r'-osx-min\d+$'),
# Newer pattern, e.g. mixxx-2.5.0.c45818.r30bca40dad-arm64-osx-min1100-release
re.compile(r'^mixxx-[\d\.]+\.c\d+\.r(\w+)-' + base_triplet_pattern + r'$'),
# New pattern, e.g. mixxx-arm64-osx-min1100-2.5.0.c45818.r30bca40dad
re.compile(r'^mixxx-' + re.escape(arch) + r'-osx-min\d+-[\d\.]+\.c\d+\.r(\w+)$'),
re.compile(r'^mixxx-' + base_triplet_pattern + r'-[\d\.]+\.c\d+\.r(\w+)$'),
# Old pattern, e.g. mixxx-2.5.0.c45816.r7c1bb1b997
*([re.compile(r'^mixxx-[\d\.]+\.c\d+\.r(\w+)$')] if arch == 'arm64' else []),
]
Expand Down
2 changes: 1 addition & 1 deletion mixxx_bisect/repository/mixxx_org.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def __init__(self, branch: str, suffix: str, opts: Options):
}.get(opts.arch)

if arch is None:
raise UnsupportedArchError(f'The architecture {opts.arch} is not supported by the mixxx.org hoster.')
raise UnsupportedArchError(f'The architecture {opts.arch} is not supported by the mixxx.org repository.')

self.snapshot_name_patterns = [
# New pattern, e.g. mixxx-2.4-alpha-6370-g44f29763ed-macosintel
Expand Down
5 changes: 5 additions & 0 deletions mixxx_bisect/runner/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ class SnapshotRunner(Protocol):
def __init__(self, opts: Options):
pass

@property
def suffix(self) -> str:
'''The file extension for the downloaded artifact.'''
raise NotImplementedError()

@property
def download_path(self) -> Path:
'''The path to download to.'''
Expand Down
38 changes: 38 additions & 0 deletions mixxx_bisect/runner/linux.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from pathlib import Path

from mixxx_bisect.options import Options
from mixxx_bisect.runner import SnapshotRunner
from mixxx_bisect.utils.run import run

import shutil

class LinuxSnapshotRunner(SnapshotRunner):
def __init__(self, opts: Options):
self.opts = opts

@property
def suffix(self) -> str:
return '.tar.gz'

@property
def download_path(self) -> Path:
return self.opts.downloads_dir / f'mixxx-current{self.suffix}'

def setup_snapshot(self):
print('Extracting snapshot...')
shutil.unpack_archive(self.download_path, self.opts.installs_dir)

def run_snapshot(self):
print('Running snapshot...')
child_dirs = list(self.opts.installs_dir.iterdir())
assert len(child_dirs) == 1, 'Mixxx should be extracted to exactly one folder'
mixxx_dir = child_dirs[0]
run([str(mixxx_dir / 'bin' / 'mixxx')], opts=self.opts)

def cleanup_snapshot(self):
print('Cleaning up snapshot...')
for path in self.opts.installs_dir.iterdir():
if path.is_file():
path.unlink()
else:
shutil.rmtree(path)
9 changes: 7 additions & 2 deletions mixxx_bisect/runner/macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,24 @@

class MacOSSnapshotRunner(SnapshotRunner):
def __init__(self, opts: Options):
self.mount_dir = opts.mount_dir
self.mount_dir = opts.installs_dir / 'mnt'
self.opts = opts

@property
def suffix(self) -> str:
return '.dmg'

@property
def download_path(self) -> Path:
return self.opts.downloads_dir / 'mixxx-current.dmg'
return self.opts.downloads_dir / f'mixxx-current{self.suffix}'

@property
def cdr_path(self) -> Path:
return self.download_path.with_suffix('.cdr')

def _mount_snapshot(self):
print('Mounting snapshot...')
self.mount_dir.mkdir(parents=True, exist_ok=True)
self.cdr_path.unlink(missing_ok=True)
run(['hdiutil', 'convert', str(self.download_path), '-format', 'UDTO', '-o', str(self.cdr_path)], opts=self.opts)
run(['hdiutil', 'attach', str(self.cdr_path), '-mountpoint', str(self.mount_dir)], opts=self.opts)
Expand Down
13 changes: 8 additions & 5 deletions mixxx_bisect/runner/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,24 @@

class WindowsSnapshotRunner(SnapshotRunner):
def __init__(self, opts: Options):
self.mount_dir = opts.mount_dir
self.install_dir = opts.mount_dir / 'Mixxx'
self.install_dir = opts.installs_dir / 'Mixxx'
self.opts = opts

@property
def suffix(self) -> str:
return '.msi'

@property
def download_path(self) -> Path:
return self.opts.downloads_dir / 'mixxx-current.msi'
return self.opts.downloads_dir / f'mixxx-current{self.suffix}'

def setup_snapshot(self):
print('Extracting snapshot...')
run([
'msiexec',
'/a', str(self.download_path), # Install the msi
'/q', # Install quietly i.e. without GUI
f'TARGETDIR={self.opts.mount_dir}', # Install to custom target dir
f'TARGETDIR={self.opts.installs_dir}', # Install to custom target dir
'/li', str(self.opts.log_dir / 'msi-install.log'), # Log installation to file
], opts=self.opts)

Expand All @@ -32,7 +35,7 @@ def run_snapshot(self):

def cleanup_snapshot(self):
print('Cleaning up snapshot...')
for path in self.opts.mount_dir.iterdir():
for path in self.opts.installs_dir.iterdir():
if path.is_file():
path.unlink()
else:
Expand Down