diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1799e9d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/git-clone/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a5e4831 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +help: + @echo 'available commands: test, test-git, test-parser' + +git-clone: + git clone --depth 1 https://github.com/Homebrew/homebrew-core/ git-clone + +test-git: git-clone + python3 -c 'import test; test.testCoreFormulae()' + +test-parser: + python3 -c 'import test; test.testConfigVariations()' + +test: test-git test-parser diff --git a/brew.py b/brew.py index 4412cee..4116a15 100755 --- a/brew.py +++ b/brew.py @@ -29,7 +29,7 @@ from argparse import ( _MutuallyExclusiveGroup as ArgsXorGroup, ) from typing import ( - Any, Callable, Iterable, Iterator, NamedTuple, Optional, Pattern, TypedDict, TypeVar + Any, Callable, Iterable, Iterator, NamedTuple, Optional, TypedDict, TypeVar ) @@ -97,7 +97,7 @@ def cli_info(args: ArgParams) -> None: Log.info(' ') else: localDeps = Cellar.getDependencies(args.package, ver) - Log.info(' ', ', '.join(localDeps) if localDeps else '') + Log.info(' ', ', '.join(sorted(localDeps)) or '') Log.info() Utils.ask('search online?') or exit(0) @@ -1324,21 +1324,10 @@ class Cellar: # Ruby file processing @staticmethod - def grepFormula(pkg: str, version: str, pattern: Pattern) \ - -> 'list[str]|None': - ''' Parse ruby file to extract information ''' - path = Cellar.rubyPath(pkg, version) - if os.path.isfile(path): - with open(path, 'r') as fp: - return pattern.findall(fp.read()) - return None - - @staticmethod - def getDependencies(pkg: str, version: str) -> 'list[str]|None': + def getDependencies(pkg: str, version: str) -> set[str]: ''' Extract dependencies from ruby file ''' assert version, 'version is required' - rx = re.compile(r'depends_on\s*"([^"]*)"(?!\s*=>)') # w/o build deps - return Cellar.grepFormula(pkg, version, rx) + return RubyParser(Cellar.rubyPath(pkg, version)).parse().dependencies @staticmethod def getHomepageUrl(pkg: str) -> 'str|None': @@ -1346,17 +1335,13 @@ class Cellar: info = Cellar.info(pkg) ver = info.verActive or ([None] + info.verAll)[-1] if ver: - rx = re.compile(r'homepage\s*"([^"]*)"') - url = Cellar.grepFormula(pkg, ver, rx) - if url: - return url[0] + return RubyParser(Cellar.rubyPath(pkg, ver)).parseHomepageUrl() return None @staticmethod def isKegOnly(pkg: str, version: str) -> bool: ''' Check if package is keg-only ''' - rx = re.compile(r'[\^\n]\s*keg_only\s*') - return len(Cellar.grepFormula(pkg, version, rx) or []) > 0 + return RubyParser(Cellar.rubyPath(pkg, version)).parseKegOnly() # ----------------------------------- @@ -1434,6 +1419,363 @@ class Fixer: os.utime(fname, (atime, mtime)) +# ----------------------------------- +# RubyParser +# ----------------------------------- + +class RubyParser: + PRINT_PARSE_ERRORS = True + ASSERT_KNOWN_SYMBOLS = False + IGNORE_RULES = False + FAKE_INSTALLED = set() # type: set[str] # simulate Cellar.info().installed + + IGNORED_TARGETS = set([':optional', ':build', ':test']) + TARGET_SYMBOLS = IGNORED_TARGETS.union([':recommended']) + # https://rubydoc.brew.sh/MacOSVersion.html#SYMBOLS-constant + # MACOS_SYMBOLS = set([':' + x for x in Arch.ALL_OS]) + # https://rubydoc.brew.sh/RuboCop/Cask/Constants#ON_SYSTEM_METHODS-constant + # https://rubydoc.brew.sh/Homebrew/SimulateSystem.html#arch_symbols-class_method + # SYSTEM_SYMBOLS = set([':arm,', ':intel', ':arm64', ':x86_64']) + # KNOWN_SYMBOLS = MACOS_SYMBOLS | SYSTEM_SYMBOLS | TARGET_SYMBOLS + + def __init__(self, path: str) -> None: + self.invalidArch = [] # type: list[str] # reasons why not supported + self.path = path + if not os.path.isfile(self.path): + raise FileNotFoundError(path) + + def readlines(self) -> Iterator[str]: + with open(self.path, 'r') as fp: + for line in fp.readlines(): + line = line.strip() + if line and not line.startswith('#'): + yield line + + def parseHomepageUrl(self) -> 'str|None': + ''' Extract homepage url ''' + for line in self.readlines(): + if line.startswith('homepage '): + return line.split('"')[1] + return None + + def parseKegOnly(self) -> bool: + ''' Check if package is keg-only ''' + for line in self.readlines(): + if line == 'keg_only' or line.startswith('keg_only '): + return True + return False + + def parse(self) -> 'RubyParser': + ''' Extract depends_on rules (updates `.invalidArch`) ''' + END = r'\s*(?:#|$)' # \n or comment + STR = r'"([^"]*)"' # "foo" + ACT = r'([^\s:]+:)' # foo: + SYM = r'(:[^\s:]+)' # :foo + ARR = r'\[([^\]]*)\]' # [foo] + TOK = fr'(?:{STR}|{SYM}|{ARR})' # "str" | :sym | [arr] + TGT = fr'(?:\s*=>\s*{TOK})?' # OPTIONAL: => {TOK} + # depends_on + DEP = fr'(?:{STR}|{SYM}|{ACT}\s+{TOK})' # "str" | :sym | act: {TOK} + IF = r'(?:\s+if\s+(.*))?' # OPTIONAL: if MacOS.version >= :catalina + # uses_from_macos + REQ = fr'(?:,\s+{ACT}\s+{SYM})?' # OPTIONAL: , act: :sym (with comma) + + rx_grp = re.compile(fr'^on_([^\s]*)\s*(.*)\s+do{END}') + rx_dep = re.compile(fr'^depends_on\s+{DEP}{TGT}{IF}{END}') + rx_use = re.compile(fr'^uses_from_macos\s+{STR}{TGT}{REQ}{END}') + + self.dependencies = set() # type: set[str] + context = [True] + prev_classes = set() # type: set[str] + for line in self.readlines(): + if line.startswith('class '): + prev_classes.add(line.split()[1]) + + if line.startswith('on_'): + if match := rx_grp.match(line): + flag = self._parse_block(*match.groups()) + context.append(flag) + else: + # ignore single outlier cvs.rb + if not line.startswith('on_macos { patches'): + self._err(line) + + elif line == 'end' or line.startswith('end '): + if len(context) > 1: + context.pop() + + elif not self.IGNORE_RULES and not all(context): + continue + + elif line.startswith('depends_on '): + if match := rx_dep.match(line): + if self._parse_depends(*match.groups()): + self.dependencies.add(match.group(1)) + else: + # glibc seems to be the only formula with weird defs + # https://github.com/Homebrew/homebrew-core/blob/main/Formula/g/glibc%402.17.rb + if line.split()[1] not in prev_classes: + self._err(line) + + elif line.startswith('uses_from_macos '): + if match := rx_use.match(line): + if not self._parse_uses(*match.groups()): + self.dependencies.add(match.group(1)) + else: + self._err(line) + + return self + + ################################################## + # Helper methods + ################################################## + + def _err(self, *msg: Any) -> None: + if self.PRINT_PARSE_ERRORS: + Log.warn('ruby parse err //', *msg, '--', self.path) + + def _unify_tok(self, string: str, sym: str, arr: str) -> list[str]: + if string: + return [string] + if sym: + return [sym] + if arr: + return [x.strip().strip('"') for x in arr.split(',')] + return [] + + def _is_ignored_target(self, args: list[str]) -> bool: + ''' Returns `True` if target is :build or :test (unless debugging) ''' + if self.ASSERT_KNOWN_SYMBOLS: + if unkown := set(args) - RubyParser.TARGET_SYMBOLS: + self._err('unkown symbol', unkown) + if self.IGNORE_RULES: + return False + for value in args: + if value in self.IGNORED_TARGETS: + return True + return False # fallback to required + + ################################################## + # on_xxx block + ################################################## + + def _parse_block(self, block: str, param: str) -> bool: + ''' Returns `True` if on_BLOCK matches requirements ''' + # https://github.com/Homebrew/brew/blob/main/Library/Homebrew/ast_constants.rb#L32 + # on_macos, on_system, on_linux, on_arm, on_intel, "on_#{os_name}" + if block == 'macos': + if not param: + return Arch.IS_MAC + elif block == 'linux': + if not param: + return not Arch.IS_MAC + elif block == 'arm': + if not param: + return Arch.IS_ARM + elif block == 'intel': + if not param: + return not Arch.IS_ARM + elif block == 'arch': + return self._eval_on_arch(param) + elif block == 'system': + if param: + return any(self._eval_on_system(x) for x in param.split(',')) + elif block in Arch.ALL_OS: + if not Arch.IS_MAC: + return False + return self._eval_on_mac_version(block, param) + self._err(f'unknown on_{block} with param "{param}"') + return True # fallback to is-a-matching-block + + def _eval_on_arch(self, param: str) -> bool: + if param == ':arm': + return Arch.IS_ARM + if param in ':intel': + return not Arch.IS_ARM + self._err(f'unknown on_arch param "{param}"') + return True # fallback to is-matching + + def _eval_on_system(self, param: str) -> bool: + ''' Returns `True` if current machine matches requirements ''' + param = param.strip() + if param == ':linux': + return not Arch.IS_MAC + + if param == ':macos': + return Arch.IS_MAC + + if param.startswith('macos: :'): + if not Arch.IS_MAC: + return False + os_name = param.removeprefix('macos: :') + if os_name.endswith('_or_older'): + if ver := Arch.ALL_OS.get(os_name.removesuffix('_or_older')): + return Arch.OS_VER <= ver + elif os_name.endswith('_or_newer'): + if ver := Arch.ALL_OS.get(os_name.removesuffix('_or_newer')): + return Arch.OS_VER >= ver + elif ver := Arch.ALL_OS.get(os_name): + return Arch.OS_VER == ver + + self._err(f'unknown on_system param "{param}"') + return True # fallback to is-matching + + def _eval_on_mac_version(self, macver: str, param: str) -> bool: + ''' Returns `True` if current machine matches requirements ''' + if not param: + return Arch.OS_VER == Arch.ALL_OS[macver] + if param == ':or_older': + return Arch.OS_VER <= Arch.ALL_OS[macver] + if param == ':or_newer': + return Arch.OS_VER >= Arch.ALL_OS[macver] + self._err(f'unknown on_{macver} param "{param}"') + return True # fallback to is-matching + + ################################################## + # uses_from_macos + ################################################## + + def _parse_uses( + self, dep: str, uStr: str, uSym: str, uArr: str, rAct: str, rSym: str, + ) -> bool: + ''' Returns `True` if requirement is fulfilled. ''' + # dep [=> :uSym|uArr]? [, rAct: :rSym]? + if self._is_ignored_target(self._unify_tok(uStr, uSym, uArr)): + return True # only a :build target + + if not Arch.IS_MAC: + return False # on linux, install + + if not rAct: + assert not rSym + return True # no need to install, because it is a Mac + + assert rSym + if rAct == 'since:': + if os_ver := Arch.ALL_OS.get(rSym.lstrip(':')): + return Arch.OS_VER >= os_ver + self._err('unknown uses_from_macos', rAct, rSym) + return True # dont install, assuming it should be fine on any Mac + + ################################################## + # depends_on + ################################################## + + def _parse_depends( + self, dep: str, sym: str, act: str, + dStr: str, dSym: str, dArr: str, + tStr: str, tSym: str, tArr: str, tIf: str, + ) -> bool: + ''' Returns `True` if dependency is required (needs install). ''' + # (dep|:sym|act: (dStr|:dSym|dArr))! [=> (tStr|:tSym|tArr)]? [if tIf]? + if sym: + self._validity_symbol(sym) + return False # no dependency, only a system requirement + + if act: + dTok = self._unify_tok(dStr, dSym, dArr) + param = dTok.pop(0) + if not self._is_ignored_target(dTok): + self._validity_action(act, param, dTok) + return False # no dependency, only a system requirement + + if self._is_ignored_target(self._unify_tok(tStr, tSym, tArr)): + return False # only a :build target + + if tIf and not self._eval_depends_if(tIf): + return False # if-clause says "no need to install" + return True # needs install + + def _eval_depends_if(self, clause: str) -> bool: + ''' Returns `True` if if-clause evaluates to True ''' + if clause.startswith('MacOS.version '): + if not Arch.IS_MAC: + return False + what, op, os_name = clause.split() + if os_ver := Arch.ALL_OS.get(os_name.lstrip(':')): + return Utils.cmpVersion(Arch.OS_VER, op, os_ver) + + elif clause.startswith('Formula["') and \ + clause.endswith('"].any_version_installed?'): + pkg = clause.split('"')[1] + return Cellar.info(pkg).installed or pkg in self.FAKE_INSTALLED + + elif clause.startswith('build.with? "'): + pkg = clause.split('"')[1] + # technically not correct, dependency could appear after this rule + return pkg in self.dependencies + + elif clause.startswith('build.without? "'): + pkg = clause.split('"')[1] + # technically not correct, dependency could appear after this rule + return pkg not in self.dependencies + + elif match := re.match(r'^(.+)\s+([<=>]+)\s+([0-9.]+)$', clause): + what, op, ver = match.groups() + ver = [int(x) for x in ver.split('.')] + if what == 'DevelopmentTools.clang_build_version': + return Utils.cmpVersion(Arch.getClangBuildVersion(), op, ver) + if what.startswith('DevelopmentTools.gcc_version'): + return Utils.cmpVersion(Arch.getGccVersion(), op, ver) + + self._err('unhandled depends_on if-clause', clause) + return True # in case of doubt, install + + ################################################## + # Check system architecture + ################################################## + + def _validArch(self, check: bool, desc: str) -> None: + if not check: + self.invalidArch.append(desc) + + def _validity_symbol(self, sym: str) -> None: + ''' Check if symbol corresponds to current system architecture ''' + if sym == ':linux': + self._validArch(not Arch.IS_MAC, 'Linux only') + elif sym == ':macos': + self._validArch(Arch.IS_MAC, 'MacOS only') + elif sym == ':xcode': + self._validArch(Arch.hasXcodeVer('1'), 'needs Xcode') + else: + self._err('unknown depends_on symbol', sym) + + def _validity_action(self, act: str, param: str, flags: list[str]) -> None: + ''' Check if action is valid on current system architecture ''' + # https://github.com/Homebrew/brew/blob/main/Library/Homebrew/dependency_collector.rb#L161 + # arch:, macos:, maximum_macos:, xcode: + # not supported (yet): linux:, codesign: + if act == 'arch:': + assert not flags + if param == ':x86_64': + self._validArch(not Arch.IS_ARM, 'no ARM support') + elif param == ':arm64': + self._validArch(Arch.IS_ARM, 'ARM only') + else: + self._err('unknown depends_on arch:', param) + + elif act in ['macos:', 'maximum_macos:']: + if os_ver := Arch.ALL_OS.get(param.lstrip(':')): + op = '<=' if act == 'maximum_macos:' else '>=' + if Arch.IS_MAC: + self._validArch(Utils.cmpVersion(Arch.OS_VER, op, os_ver), + f'needs macOS {op} {os_ver}') + else: + self._validArch(False, f'needs macOS {op} {os_ver}') + else: + self._err('unknown depends_on', act, param) + + elif act == 'xcode:': + ver = param + if ver.startswith(':'): # probably some ":build" + self._validArch(Arch.hasXcodeVer('1'), 'needs Xcode') + else: + self._validArch(Arch.hasXcodeVer(ver), f'needs Xcode >= {ver}') + + else: + self._err('unknown depends_on action', act, param, flags) + + # ----------------------------------- # Utils # ----------------------------------- diff --git a/test-formula.rb b/test-formula.rb new file mode 100644 index 0000000..d6b9631 --- /dev/null +++ b/test-formula.rb @@ -0,0 +1,245 @@ +class Test < Formula + homepage "https://example.org" + keg_only :macos + + depends_on xcode: "8.3" + + depends_on :macos + depends_on :linux + + depends_on arch: :x86_64 + depends_on arch: :arm64 + + # test build target + depends_on "__:build__:test__" => [:build, :test] + depends_on "__:build__" => :build + depends_on "__:test__" => :test + depends_on "__:recommended__" => :recommended + depends_on "__:optional__" => :optional + + # test uses_from_macos + uses_from_macos "__uses_from_macos__" + uses_from_macos "__uses_from_macos__:build__" => :build + uses_from_macos "__uses_from_macos__:build__since__" => :build, since: :catalina + uses_from_macos "__uses_from_macos__since_catalina__", since: :catalina + uses_from_macos "__uses_from_macos__since_sierra__", since: :sierra + + # test if-clause + depends_on "__if_clang_<=_1400__" if DevelopmentTools.clang_build_version <= 1400 + depends_on "__if_gcc_<_9__" if DevelopmentTools.gcc_version("/usr/bin/gcc") < 9 + depends_on "__if_macos_>=_catalina__" if MacOS.version >= :catalina + depends_on "__if_any_zlib_installed__" if Formula["zlib"].any_version_installed? + depends_on "__if_build.with_catalina__" if build.with? "__if_macos_>=_catalina__" + depends_on "__if_build.without_catalina__" if build.without? "__if_macos_>=_catalina__" + + + on_macos do + depends_on "__on_macos__" + end + + on_linux do + depends_on "__on_linux__" + end + + on_macos do + on_linux do + depends_on "__nested__on_macos__on_linux__" + end + end + + on_linux do + on_macos do + depends_on "__nested__on_linux__on_macos__" + end + end + + # https://rubydoc.brew.sh/OnSystem/MacOSAndLinux.html + # https://rubydoc.brew.sh/Formula.html + # https://rubydoc.brew.sh/Formula#uses_from_macos-class_method + # https://rubydoc.brew.sh/Cask/DSL/DependsOn.html + + ############################################################################# + # from https://rubydoc.brew.sh/Formula.html#on_system_blocks_exist%3F-class_method + # + on_monterey :or_older do + depends_on "__on_monterey :or_older__" + end + on_system :linux, macos: :big_sur_or_newer do + depends_on "__on_system :linux, macos: :big_sur_or_newer__" + end + # + ############################################################################# + + + ############################################################################# + # from https://rubydoc.brew.sh/OnSystem.html#ALL_OS_OPTIONS-constant + # + on_arch :arm do # comment + depends_on "__on_arch :arm__" + end + on_arch :intel do + depends_on "__on_arch :intel__" + end + on_system macos: :sierra_or_older do + depends_on "__on_system macos: :sierra_or_older__" + end + # + ############################################################################# + + + ############################################################################# + # https://rubydoc.brew.sh/RuboCop/Cask/AST/Stanza.html#on_arch_conditional%3F-instance_method + # + on_arm do + depends_on "__on_arm__" + end + on_intel do + depends_on "__on_intel__" + end + # + # + on_yosemite do + depends_on "__on_yosemite__" + end + on_el_capitan do + depends_on "__on_el_capitan__" + end + on_sierra do + depends_on "__on_sierra__" + end + on_high_sierra do + depends_on "__on_high_sierra__" + end + on_mojave do + depends_on "__on_mojave__" + end + on_catalina do + depends_on "__on_catalina__" + end + on_big_sur do + depends_on "__on_big_sur__" + end + on_monterey do + depends_on "__on_monterey__" + end + on_ventura do + depends_on "__on_ventura__" + end + on_sonoma do + depends_on "__on_sonoma__" + end + on_sequoia do + depends_on "__on_sequoia__" + end + on_tahoe do + depends_on "__on_tahoe__" + end + # + ############################################################################# + + + ############################################################################# + # from glib formula + # + depends_on "python-setuptools" => :build # for gobject-introspection + depends_on "python@3.13" => :build + depends_on "pcre2" + # + uses_from_macos "flex" => :build # for gobject-introspection + uses_from_macos "libffi", since: :catalina + uses_from_macos "python" + uses_from_macos "zlib" + # + ############################################################################# + + + ############################################################################# + # from https://docs.brew.sh/Formula-Cookbook#specifying-macos-components-as-dependencies + # + # For example, to require the bzip2 formula on Linux while relying on built-in bzip2 on macOS: + uses_from_macos "bzip2" + # To require the perl formula only when building or testing on Linux: + uses_from_macos "perl" => [:build, :test] + # To require the curl formula on Linux and pre-macOS 12: + uses_from_macos "curl", since: :monterey + # + ############################################################################# + + + ############################################################################# + # from https://github.com/Homebrew/homebrew-core/blob/main/Formula/c/c-blosc2.rb + # + on_macos do + depends_on "llvm" => :build if DevelopmentTools.clang_build_version <= 1400 + end + # + ############################################################################# + + + ############################################################################# + # from https://docs.brew.sh/Formula-Cookbook#specifying-other-formulae-as-dependencies + # + depends_on "httpd" => [:build, :test] + depends_on xcode: ["9.3", :build] + depends_on arch: :x86_64 + depends_on "jpeg" + depends_on macos: :high_sierra + depends_on "pkg-config" + depends_on "readline" => :recommended + depends_on "gtk+" => :optional + # + option "with-foo", "Compile with foo bindings" # This overrides the generated description if you want to + depends_on "foo" => :optional # Generated description would otherwise be "Build with foo support" + # + ############################################################################# + + + ############################################################################# + # from https://docs.brew.sh/Formula-Cookbook#handling-different-system-configurations + # + on_linux do + depends_on "gcc" + end + # + on_mojave :or_newer do # comment + depends_on "gettext" => :build + end + # + on_system :linux, macos: :sierra_or_older do # comment + depends_on "gettext" => :build # comment + end + # + on_macos do # comment + on_arm do + depends_on "gettext" => :build + end + end + # + ############################################################################# + + + ############################################################################# + # from https://rubydoc.brew.sh/Formula#depends_on-class_method + # + # :build means this dependency is only needed during build. + depends_on "cmake" => :build + # :test means this dependency is only needed during testing. + depends_on "node" => :test + # :recommended dependencies are built by default. But a --without-... option is generated to opt-out. + depends_on "readline" => :recommended + # :optional dependencies are NOT built by default unless the auto-generated --with-... option is passed. + depends_on "glib" => :optional + # If you need to specify that another formula has to be built with/out certain options (note, no -- needed before the option): + depends_on "zeromq" => "with-pgm" + depends_on "qt" => ["with-qtdbus", "developer"] # Multiple options. + # Optional and enforce that "boost" is built using --with-c++11. + depends_on "boost" => [:optional, "with-c++11"] + # If a dependency is only needed in certain cases: + depends_on "sqlite" if MacOS.version >= :catalina + depends_on xcode: :build # If the formula really needs full Xcode to compile. + depends_on macos: :mojave # Needs at least macOS Mojave (10.14) to run. + # It is possible to only depend on something if build.with? or build.without? "another_formula": + depends_on "postgresql" if build.without? "sqlite" + # + ############################################################################# +end diff --git a/test.py b/test.py new file mode 100644 index 0000000..18dc92b --- /dev/null +++ b/test.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +import os +from brew import Arch, RubyParser + +RubyParser.PRINT_PARSE_ERRORS = True +RubyParser.ASSERT_KNOWN_SYMBOLS = True +RubyParser.IGNORE_RULES = True + +Arch.OS_VER = '0' +Arch.IS_MAC = True +Arch.IS_ARM = True +Arch._SOFTWARE_VERSIONS = { + 'xcode': [0], + 'gcc': [0], + 'clang': [0], +} + + +def main() -> None: + # testCoreFormulae() + testRubyTestFile() + # testConfigVariations() + + +def testRubyTestFile() -> None: + ruby = RubyParser('test-formula.rb').parse() + print() + print('deps:') + for dep in sorted(ruby.dependencies): + if dep.startswith('__'): + print(' ', dep) + print('invalid arch:') + print(' ', ruby.invalidArch) + + +def testCoreFormulae() -> None: + if not os.path.isdir('git-clone'): + print('run `make git-clone` first') + return + + RubyParser.PRINT_PARSE_ERRORS = True + RubyParser.ASSERT_KNOWN_SYMBOLS = True + RubyParser.IGNORE_RULES = True + + for x in os.scandir('git-clone/Formula'): + if x.is_dir(): + for file in os.scandir(x.path): + RubyParser(file.path).parse() + + +def testConfigVariations() -> None: + RubyParser.PRINT_PARSE_ERRORS = False + RubyParser.ASSERT_KNOWN_SYMBOLS = False + RubyParser.IGNORE_RULES = False + + for ver in Arch.ALL_OS.values(): + Arch.OS_VER = ver + for ismac in [True, False]: + Arch.IS_MAC = ismac + for isarm in [True, False]: + Arch.IS_ARM = isarm + for xcode in [0, 9, 15]: + Arch._SOFTWARE_VERSIONS['xcode'] = [xcode] + for clang in [0, 1300, 1700]: + Arch._SOFTWARE_VERSIONS['clang'] = [clang] + for gcc in [0, 8, 14]: + Arch._SOFTWARE_VERSIONS['gcc'] = [gcc] + runSingleParseTest() + + RubyParser.FAKE_INSTALLED.add('zlib') + runSingleParseTest() + RubyParser.FAKE_INSTALLED.clear() + runSingleParseTest() + print('ok') + + +def runSingleParseTest() -> None: + ruby = RubyParser('test-formula.rb').parse() + assertInvalidArch(ruby) + assertDependencies(ruby.dependencies) + + +def assertInvalidArch(ruby: RubyParser) -> None: + if Arch.IS_ARM: + assert 'no ARM support' in ruby.invalidArch + assert 'ARM only' not in ruby.invalidArch + else: + assert 'no ARM support' not in ruby.invalidArch + assert 'ARM only' in ruby.invalidArch + + if Arch._SOFTWARE_VERSIONS['xcode'] < [1]: + assert 'needs Xcode >= 8.3' in ruby.invalidArch + assert 'needs Xcode' in ruby.invalidArch + elif Arch._SOFTWARE_VERSIONS['xcode'] < [8, 3]: + assert 'needs Xcode >= 8.3' in ruby.invalidArch + assert 'needs Xcode' not in ruby.invalidArch + else: + assert 'needs Xcode >= 8.3' not in ruby.invalidArch + assert 'needs Xcode' not in ruby.invalidArch + + if not Arch.IS_MAC: + assert 'Linux only' not in ruby.invalidArch + assert 'needs macOS >= 10.13' in ruby.invalidArch + assert 'needs macOS >= 10.14' in ruby.invalidArch + assert 'MacOS only' in ruby.invalidArch + elif Arch.OS_VER < '10.13': + assert 'Linux only' in ruby.invalidArch + assert 'needs macOS >= 10.13' in ruby.invalidArch + assert 'needs macOS >= 10.14' in ruby.invalidArch + elif Arch.OS_VER < '10.14': + assert 'Linux only' in ruby.invalidArch + assert 'needs macOS >= 10.13' not in ruby.invalidArch + assert 'needs macOS >= 10.14' in ruby.invalidArch + else: + assert 'Linux only' in ruby.invalidArch + assert 'needs macOS >= 10.13' not in ruby.invalidArch + assert 'needs macOS >= 10.14' not in ruby.invalidArch + + +def assertDependencies(deps: set[str]) -> None: + # test build target + + assert '__:recommended__' in deps + assert '__:build__:test__' not in deps + assert '__:build__' not in deps + assert '__:test__' not in deps + assert '__:optional__' not in deps + + # test nested ignore + + assert '__nested__on_macos__on_linux__' not in deps + assert '__nested__on_linux__on_macos__' not in deps + + # test macos versions + for os_name, os_ver in Arch.ALL_OS.items(): + if Arch.IS_MAC and Arch.OS_VER == os_ver: + assert f'__on_{os_name}__' in deps, f'{os_name} in deps' + else: + assert f'__on_{os_name}__' not in deps, f'{os_name} not in deps' + + if Arch.IS_MAC: + assert '__on_macos__' in deps + assert '__on_linux__' not in deps + assert '__uses_from_macos__' not in deps + else: + assert '__on_linux__' in deps + assert '__on_macos__' not in deps + assert '__uses_from_macos__' in deps + + if Arch.IS_ARM: + assert '__on_arm__' in deps + assert '__on_intel__' not in deps + assert '__on_arch :arm__' in deps + assert '__on_arch :intel__' not in deps + else: + assert '__on_arm__' not in deps + assert '__on_intel__' in deps + assert '__on_arch :arm__' not in deps + assert '__on_arch :intel__' in deps + + if Arch.IS_MAC and Arch.OS_VER <= '12': + assert '__on_monterey :or_older__' in deps + else: + assert '__on_monterey :or_older__' not in deps + + if Arch.OS_VER <= '10.12' and Arch.IS_MAC: + assert '__on_system macos: :sierra_or_older__' in deps + else: + assert '__on_system macos: :sierra_or_older__' not in deps + + if Arch.IS_MAC and Arch.OS_VER < '11': + assert '__on_system :linux, macos: :big_sur_or_newer__' not in deps + else: + assert '__on_system :linux, macos: :big_sur_or_newer__' in deps + + # test uses_from_macos + + assert '__uses_from_macos__:build__' not in deps + assert '__uses_from_macos__:build__since__' not in deps + + if Arch.OS_VER >= '10.15' and Arch.IS_MAC: + assert '__uses_from_macos__since_catalina__' not in deps + assert '__uses_from_macos__since_sierra__' not in deps + elif Arch.OS_VER >= '10.12' and Arch.IS_MAC: + assert '__uses_from_macos__since_catalina__' in deps + assert '__uses_from_macos__since_sierra__' not in deps + else: + assert '__uses_from_macos__since_catalina__' in deps + assert '__uses_from_macos__since_sierra__' in deps + + # test if-clause + + if Arch.OS_VER >= '10.15' and Arch.IS_MAC: + assert '__if_macos_>=_catalina__' in deps + assert '__if_build.with_catalina__' in deps + assert '__if_build.without_catalina__' not in deps + else: + assert '__if_build.with_catalina__' not in deps + assert '__if_build.without_catalina__' in deps + assert '__if_macos_>=_catalina__' not in deps + + if Arch._SOFTWARE_VERSIONS['clang'] <= [1400]: + assert '__if_clang_<=_1400__' in deps + else: + assert '__if_clang_<=_1400__' not in deps + + if Arch._SOFTWARE_VERSIONS['gcc'] < [9]: + assert '__if_gcc_<_9__' in deps + else: + assert '__if_gcc_<_9__' not in deps + + if 'zlib' in RubyParser.FAKE_INSTALLED: + assert '__if_any_zlib_installed__' in deps + else: + assert '__if_any_zlib_installed__' not in deps + + +if __name__ == '__main__': + main()