From a1a13ee34c51e2eb3d1b55f9d0e91cec3d176b96 Mon Sep 17 00:00:00 2001 From: isen <3904043+isen-ng@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:09:10 +0800 Subject: [PATCH 1/7] Fix python tests --- .github/workflows/ci.yml | 8 ++++++++ test_meta_selector.py | 9 ++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7bf626fc3..71be57d2a 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,15 @@ env: HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: + python-test: + runs-on: ubuntu-latest + steps: + - name: Run python tests + id: python-tests + run: python -m unittest + generate-matrix: + needs: python-tests outputs: matrix: ${{ steps.generate-matrix.outputs.matrix }} runs-on: ubuntu-latest diff --git a/test_meta_selector.py b/test_meta_selector.py index 7e27d648d..77182a269 100644 --- a/test_meta_selector.py +++ b/test_meta_selector.py @@ -45,12 +45,7 @@ def test_run_should_set_url_to_meta_readme_from_current_repository(self): stdout=subprocess.PIPE ).stdout.decode("utf-8")).splitlines() interesting = [l for l in stdout if l.startswith("origin")] - first = interesting[0] - if first.find("fluffynuts") > -1: - expectedUrl = "https://raw.githubusercontent.com/fluffynuts/homebrew-dotnet-sdk-versions/master/META.md" - else: - # try to make this work when it's finally merged... - expectedUrl = "https://raw.githubusercontent.com/isen-ng/homebrew-dotnet-sdk-versions/master/META.md" + expectedUrl = "https://github.com/isen-ng/homebrew-dotnet-sdk-versions/raw/master/META.md" self.create_caskfile("7-0-200") expected_version = "7-0-400" @@ -96,7 +91,7 @@ def test_run_should_append_non_variable_lines(self): raw = self.read_work_file_raw(expected_file) self.assertContains(raw, "name \".NET Core SDK #{version.csv.first}\"") self.assertContains(raw, "desc \"This cask follows releases from https://github.com/dotnet/core/tree/master\"") - self.assertContains(raw, "homepage \"https://www.microsoft.com/net/core#macos\"") + self.assertContains(raw, "homepage \"https://github.com/isen-ng/homebrew-dotnet-sdk-versions\"") def test_run_should_not_duplicate_depends_on(self): self.create_caskfile("7-0-200") From a29f799bb857e52c605ee6bce68b7ee74d72506f Mon Sep 17 00:00:00 2001 From: isen <3904043+isen-ng@users.noreply.github.com> Date: Mon, 2 Sep 2024 10:54:52 +0800 Subject: [PATCH 2/7] Add meta-packages to readme --- README.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8eb8137e5..ff0fe999f 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,20 @@ brew install --cask dotnet --list-sdks ``` +### Meta versions + +| Version | .NET SDK | +| ------------- | -------- | +| `dotnet-sdk8` | 8.0.401 | +| `dotnet-sdk7` | 7.0.410 | +| `dotnet-sdk6` | 6.0.425 | +| `dotnet-sdk5` | 5.0.408 | +| `dotnet-sdk4` | 3.1.426 | +| `dotnet-sdk3` | 2.2.402 | + +**Note**: These meta-packages installs the latest MINOR and PATCH versions from the existing versions list below. + +**Note 2**: See each corresponding version below for `arch` and `remarks`. ### Versions @@ -60,13 +74,10 @@ after installing/upgrading to .NET SDK 5. | Version | .NET SDK | Arch | Remarks | | --------------------- | -------------------------- | ----------- | ------- | -| `dotnet-sdk9-preview` | 9.0.100-preview.7.24407.12 | x64 & arm64 | | +| `dotnet-sdk9-preview` | 9.0.100-preview.7.24407.12 | x64 & arm64 | | | `dotnet-sdk8-preview` | 8.0.101-rc.2.23502.2 | x64 & arm64 | | | `dotnet-sdk7-preview` | 7.0.100-rc.2.22477.23 | x64 & arm64 | | -**Note**: Preview versions is a newly supported feature (as of September 2022). Please send feedback/create issues -if there are problems with the compatibility of the preview versions. - ## Uninstalling From 5ffb2d14038ac4cdc08c5db955b3e80c32139520 Mon Sep 17 00:00:00 2001 From: isen <3904043+isen-ng@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:01:02 +0800 Subject: [PATCH 3/7] Make list_work_files a static method --- meta_selector.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/meta_selector.py b/meta_selector.py index 6af47c6bd..a963e6a2d 100644 --- a/meta_selector.py +++ b/meta_selector.py @@ -46,7 +46,7 @@ def __init__(self, workdir): pass def run(self): - files = self.list_work_files() + files = self.list_work_files(self.workdir) lookup = self.generate_version_lookup(files) for key in lookup: sdk = lookup[key] @@ -191,10 +191,11 @@ def generate_version_lookup(files) -> Dict[str, Sdk]: lookup[name] = current return lookup - def list_work_files(self): + @staticmethod + def list_work_files(workdir): return [ - f for f in os.listdir(self.workdir) - if os.path.isfile(os.path.join(self.workdir, f)) + f for f in os.listdir(workdir) + if os.path.isfile(os.path.join(workdir, f)) ] def should_keep_line(self, line: str): From 97af1d67f458c3288207efc87d1c5f93f08cdbae Mon Sep 17 00:00:00 2001 From: isen <3904043+isen-ng@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:15:23 +0800 Subject: [PATCH 4/7] Organize private regexes --- meta_selector.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/meta_selector.py b/meta_selector.py index a963e6a2d..87d601bb3 100644 --- a/meta_selector.py +++ b/meta_selector.py @@ -35,12 +35,17 @@ class MetaSelector: re.compile("^\\s*desc\\b.*"), re.compile("^\\s*depends_on\\b.*") ] + __url_vars = [ "url", "url_x64", "url_arm64" ] + __web_url_regex = re.compile(".*://(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)") + __git_url_regex = re.compile(".*@(?P[a-zA-Z0-9_.-]+):(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)") + __git_ssh_regex = re.compile(".*://.*@(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)") + def __init__(self, workdir): self.workdir = workdir pass @@ -122,11 +127,6 @@ def hash(self, url: str) -> str: result = sha256(raw_data) return result.hexdigest() - __web_url_regex = re.compile(".*://(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)") - __git_url_regex = re.compile(".*@(?P[a-zA-Z0-9_.-]+):(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)") - __git_ssh_regex = re.compile( - ".*://.*@(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)") - def read_origin_remote(self) -> List[str]: lines = [str(l) for l in subprocess.check_output("git remote -v", shell=True).decode("utf-8").splitlines()] origin_lines = [l for l in lines if l.strip().startswith("origin")] From 181774abef58969477b6402eff2dbe71b7ea6b90 Mon Sep 17 00:00:00 2001 From: isen <3904043+isen-ng@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:16:49 +0800 Subject: [PATCH 5/7] Move regex compilation out from method --- meta_selector.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/meta_selector.py b/meta_selector.py index 87d601bb3..585471204 100644 --- a/meta_selector.py +++ b/meta_selector.py @@ -45,6 +45,7 @@ class MetaSelector: __web_url_regex = re.compile(".*://(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)") __git_url_regex = re.compile(".*@(?P[a-zA-Z0-9_.-]+):(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)") __git_ssh_regex = re.compile(".*://.*@(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)") + __version_regex = re.compile("(?Pdotnet-sdk)(?P\\d+)-(?P\\d+)-(?P\\d+)") def __init__(self, workdir): self.workdir = workdir @@ -166,12 +167,10 @@ def log(self, s: str) -> None: return print(s) - @staticmethod - def generate_version_lookup(files) -> Dict[str, Sdk]: - regex = re.compile("(?Pdotnet-sdk)(?P\\d+)-(?P\\d+)-(?P\\d+)") + def generate_version_lookup(self, files) -> Dict[str, Sdk]: lookup = {} for f in files: - match = regex.match(f) + match = self.__version_regex.match(f) if match is None: print("ignored {}: No version match match for filename".format(f)) continue From ce0b36fd068629604e1d4f06f3ff0536d9a827c7 Mon Sep 17 00:00:00 2001 From: isen <3904043+isen-ng@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:34:01 +0800 Subject: [PATCH 6/7] MPrefix regex strings with r --- meta_selector.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/meta_selector.py b/meta_selector.py index 585471204..a1f343487 100644 --- a/meta_selector.py +++ b/meta_selector.py @@ -42,10 +42,10 @@ class MetaSelector: "url_arm64" ] - __web_url_regex = re.compile(".*://(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)") - __git_url_regex = re.compile(".*@(?P[a-zA-Z0-9_.-]+):(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)") - __git_ssh_regex = re.compile(".*://.*@(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)") - __version_regex = re.compile("(?Pdotnet-sdk)(?P\\d+)-(?P\\d+)-(?P\\d+)") + __web_url_regex = re.compile(r".*://(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)") + __git_url_regex = re.compile(r".*@(?P[a-zA-Z0-9_.-]+):(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)") + __git_ssh_regex = re.compile(r".*://.*@(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)") + __version_regex = re.compile(r"(?Pdotnet-sdk)(?P\d+)-(?P\d+)-(?P\d+)") def __init__(self, workdir): self.workdir = workdir From 5b174d5198943176ef9bea79c2e76142189961a8 Mon Sep 17 00:00:00 2001 From: isen <3904043+isen-ng@users.noreply.github.com> Date: Mon, 2 Sep 2024 13:42:30 +0800 Subject: [PATCH 7/7] Refactor meta updater --- .github/workflows/auto-commit.yml | 4 +- .github/workflows/auto-meta-updater.yml | 28 ++ .github/workflows/ci.yml | 44 ++-- README.md | 4 +- auto_committer.py | 2 +- auto_meta_updater.py | 238 +++++++++++++++++ meta-cask.rb.template | 16 ++ meta-cask.x64.rb.template | 14 + meta_selector.py | 209 --------------- test_meta_selector.py | 333 ------------------------ 10 files changed, 321 insertions(+), 571 deletions(-) create mode 100644 .github/workflows/auto-meta-updater.yml create mode 100644 auto_meta_updater.py create mode 100644 meta-cask.rb.template create mode 100644 meta-cask.x64.rb.template delete mode 100644 meta_selector.py delete mode 100644 test_meta_selector.py diff --git a/.github/workflows/auto-commit.yml b/.github/workflows/auto-commit.yml index 768649463..4d6bb8df6 100644 --- a/.github/workflows/auto-commit.yml +++ b/.github/workflows/auto-commit.yml @@ -2,8 +2,8 @@ name: auto-committer on: schedule: - # run at 1900UTC (3am +8GMT) everyday, 2 hours after auto updater - - cron: "0 19 * * *" + # run at 1800UTC (2am +8GMT) and 2000UTC (4am +8GMT) everyday, 1 hour after both auto updaters + - cron: "0 18,20 * * *" workflow_dispatch: diff --git a/.github/workflows/auto-meta-updater.yml b/.github/workflows/auto-meta-updater.yml new file mode 100644 index 000000000..60292e5fc --- /dev/null +++ b/.github/workflows/auto-meta-updater.yml @@ -0,0 +1,28 @@ +name: auto-updater + +on: + schedule: + # run at 1900UTC (3am +8GMT) everyday, 2 hours after regular updater, 1 hour after first commiter + - cron: "0 19 * * *" + workflow_dispatch: + + +jobs: + update: + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set git username and email, so we can commit and push + run: | + git config --global user.email "3904043+isen-ng@users.noreply.github.com" + git config --global user.name "Isen Ng" + + - name: Run auto_meta_updater.py + env: + GITHUB_USER: ${{ secrets.GITHUB_USER }} + GITHUB_TOKEN: ${{ secrets.UPDATER_PUSH_TOKEN }} + run: ./auto_meta_updater.py --really_commit --really_push diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71be57d2a..157710273 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,15 +11,7 @@ env: HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: - python-test: - runs-on: ubuntu-latest - steps: - - name: Run python tests - id: python-tests - run: python -m unittest - generate-matrix: - needs: python-tests outputs: matrix: ${{ steps.generate-matrix.outputs.matrix }} runs-on: ubuntu-latest @@ -109,34 +101,38 @@ jobs: require 'cask/installer' cask = Cask::CaskLoader.load('${{ matrix.cask.path }}') - + was_installed = cask.installed? - manual_installer = cask.artifacts.any? { |artifact| - artifact.is_a?(Cask::Artifact::Installer::ManualInstaller) - } - + manual_installer = cask.artifacts.any? do |artifact| + if defined?(artifact.manual_install) + artifact.manual_install + end + end + macos_requirement_satisfied = if macos_requirement = cask.depends_on.macos macos_requirement.satisfied? else true end - + cask_conflicts = cask.conflicts_with&.dig(:cask).to_a.select { |c| Cask::CaskLoader.load(c).installed? } formula_conflicts = cask.conflicts_with&.dig(:formula).to_a.select { |f| Formula[f].any_version_installed? } - + installer = Cask::Installer.new(cask) cask_and_formula_dependencies = installer.missing_cask_and_formula_dependencies - + cask_dependencies = cask_and_formula_dependencies.select { |d| d.is_a?(Cask::Cask) }.map(&:full_name) formula_dependencies = cask_and_formula_dependencies.select { |d| d.is_a?(Formula) }.map(&:full_name) - - puts "::set-output name=was_installed::#{JSON.generate(was_installed)}" - puts "::set-output name=manual_installer::#{JSON.generate(manual_installer)}" - puts "::set-output name=macos_requirement_satisfied::#{JSON.generate(macos_requirement_satisfied)}" - puts "::set-output name=cask_conflicts::#{JSON.generate(cask_conflicts)}" - puts "::set-output name=cask_dependencies::#{JSON.generate(cask_dependencies)}" - puts "::set-output name=formula_conflicts::#{JSON.generate(formula_conflicts)}" - puts "::set-output name=formula_dependencies::#{JSON.generate(formula_dependencies)}" + + File.open(ENV.fetch("GITHUB_OUTPUT"), "a") do |f| + f.puts "was_installed=#{JSON.generate(was_installed)}" + f.puts "manual_installer=#{JSON.generate(manual_installer)}" + f.puts "macos_requirement_satisfied=#{JSON.generate(macos_requirement_satisfied)}" + f.puts "cask_conflicts=#{JSON.generate(cask_conflicts)}" + f.puts "cask_dependencies=#{JSON.generate(cask_dependencies)}" + f.puts "formula_conflicts=#{JSON.generate(formula_conflicts)}" + f.puts "formula_dependencies=#{JSON.generate(formula_dependencies)}" + end EOF if: always() && steps.gems.outcome == 'success' && matrix.cask diff --git a/README.md b/README.md index ff0fe999f..f5e7fdaf1 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ dotnet --list-sdks | `dotnet-sdk7` | 7.0.410 | | `dotnet-sdk6` | 6.0.425 | | `dotnet-sdk5` | 5.0.408 | -| `dotnet-sdk4` | 3.1.426 | -| `dotnet-sdk3` | 2.2.402 | +| `dotnet-sdk3` | 3.1.426 | +| `dotnet-sdk2` | 2.2.402 | **Note**: These meta-packages installs the latest MINOR and PATCH versions from the existing versions list below. diff --git a/auto_committer.py b/auto_committer.py index 1f71b96a7..b7245e7a7 100755 --- a/auto_committer.py +++ b/auto_committer.py @@ -21,7 +21,7 @@ def process_ready_pull_requests(self, pull_requests): for pull_request in pull_requests: git_service.merge_pull_request(pull_request) - time.sleep(10) + time.sleep(20) class GitService: diff --git a/auto_meta_updater.py b/auto_meta_updater.py new file mode 100644 index 000000000..ea8b36a90 --- /dev/null +++ b/auto_meta_updater.py @@ -0,0 +1,238 @@ +import os +import re +from typing import List, Dict +import subprocess + + +class MetaCask: + def __init__(self, meta_cask_name: str, depends_on_filename: str, major: str, minor: str, patch: str): + self.meta_cask_name = meta_cask_name + self.depends_on_filename = depends_on_filename + self.major = major + self.minor = minor + self.patch = patch + + def is_newer_than(self, other): + if self.major > other.major: + return True + elif self.major < other.major: + return False + + if self.minor > other.minor: + return True + elif self.minor < other.minor: + return False + + if self.patch > other.patch: + return True + elif self.patch < other.patch: + return False + + return False + + def __str__(self): + return f'{self.major}.{self.minor}.{self.patch}' + + +class DependsOnCask: + def __init__(self, cask_name: str, sdk_version: str, runtime_version: str, depends_on_cask: str, depends_on_macos: str, is_arm_supported: bool): + self.cask_name = cask_name + self.sdk_version = sdk_version + self.runtime_version = runtime_version + self.depends_on_cask = depends_on_cask + self.depends_on_macos = depends_on_macos + self.is_arm_supported = is_arm_supported + + +class TemplateService: + __cask_template_path = "meta-cask.rb.template"; + __cask_x64_template_path = "meta-cask.x64.rb.template"; + + def generate(self, meta_cask: MetaCask, depends_on_cask: DependsOnCask) -> str: + if depends_on_cask.is_arm_supported: + path = self.__cask_template_path + else: + path = self.__cask_x64_template_path + + with open(path, 'r') as file: + content = file.read() + + content = content.replace('{cask_name}', meta_cask.meta_cask_name) + content = content.replace('{sdk_version}', depends_on_cask.sdk_version) + content = content.replace('{runtime_version}', depends_on_cask.runtime_version) + content = content.replace('{depends_on_cask}', depends_on_cask.depends_on_cask) + content = content.replace('{depends_on_macos}', depends_on_cask.depends_on_macos) + + return content + + +class MetaLookupService: + __cask_version_regex = re.compile(r"(?Pdotnet-sdk)(?P\d+)-(?P\d+)-(?P\d+)") + + def __init__(self, cask_directory): + self.cask_directory = cask_directory + + def generate_version_lookup(self) -> Dict[str, MetaCask]: + result = {} + casks = self.__list_work_files(); + for cask_filename in casks: + match = self.__cask_version_regex.search(cask_filename) + if match is None: + print("ignored {}: No version match match for filename".format(cask_filename)) + continue + + major = match.group("major") + minor = match.group("minor") + patch = match.group("patch") + + meta_cask_name = "{}{}".format(match.group("name"), major) + current = MetaCask(meta_cask_name, cask_filename, major, minor, patch) + + if meta_cask_name not in result: + result[meta_cask_name] = current + continue + + existing = result[meta_cask_name] + if current.is_newer_than(existing): + result[meta_cask_name] = current + + return list(result.values()) + + def __list_work_files(self): + result = [] + for file in os.listdir(self.cask_directory): + if not os.path.isfile(os.path.join(self.cask_directory, file)): + continue + + if "-preview" in file: + continue + + if "-rc" in file: + continue + + # major meta cask only contains 1 dash. eg `dotnet-sdk8` + if file.count('-') is 1: + continue + + result.append(file) + + return result + + +class DependsOnCaskParser: + __cask_name_pattern = re.compile(r'cask "(?P[a-z0-9-]+)" do') + __version_pattern = re.compile(r'version "(?P\d+.\d+.\d+),(?P\d+.\d+.\d+)"') + __macos_pattern = re.compile(r'depends_on macos: "(?P[>= :a-z_]+)"') + __arm_pattern = re.compile(r'arch arm: "arm64", intel: "x64"') + + def __init__(self, cask_directory): + self.cask_directory = cask_directory + + def parse(self, meta_cask: MetaCask) -> DependsOnCask: + path = os.path.join(self.cask_directory, meta_cask.depends_on_filename) + with open(path, 'r') as file: + content = file.read() + + cask_name_match = self.__cask_name_pattern.search(content) + if cask_name_match is None: + return + + version_match = self.__version_pattern.search(content) + if version_match is None: + return + + macos_match = self.__macos_pattern.search(content) + if macos_match is None: + return + + arm_match = self.__arm_pattern.search(content) + if arm_match is None: + is_arm_supported = False + else: + is_arm_supported = True + + depends_on_cask_name = meta_cask.depends_on_filename.replace(".rb", "") + return DependsOnCask( + depends_on_cask_name, + version_match.group("sdk_version"), + version_match.group("runtime_version"), + cask_name_match.group("cask_name"), + macos_match.group("depends_on_macos"), + is_arm_supported) + + +class ReadMeUpdater: + def update(self, meta_cask: MetaCask, depends_on_cask: DependsOnCask): + file_path = 'README.md' + + with open(file_path, 'r') as file: + content = file.read() + + content = re.sub(rf'`{meta_cask.meta_cask_name}` \| [\d+.\d+.\d+]+', f'`{meta_cask.meta_cask_name}` | {depends_on_cask.sdk_version}', content) + + with open(file_path, 'w') as file: + file.write(content) + + +class GitService: + def push(self, really_commit, really_push): + branch_name = 'update-meta-casks' + commit_message = '[Auto] Update meta casks' + + if really_commit: + subprocess.run(['git', 'checkout', '-b', branch_name, 'master'], check = False) + subprocess.run(['git', 'add', '-A'], check = True) + subprocess.run(['git', 'commit', '-m', commit_message], check = True) + + if really_push: + subprocess.run(['git', 'push', 'origin', '--force', branch_name], check = True) + subprocess.run(['gh', 'pr', 'create', '--base', 'master', '--head', branch_name, '--title', commit_message, '--body', ''], check = True) + + +class MetaUpdater: + def __init__(self, cask_directory: str, + meta_lookup_service: MetaLookupService, + depends_on_cask_parser: DependsOnCaskParser, + template_service: TemplateService, + read_me_updater: ReadMeUpdater): + self.cask_directory = cask_directory + self.meta_lookup_service = meta_lookup_service + self.depends_on_cask_parser = depends_on_cask_parser + self.template_service = template_service + self.read_me_updater = read_me_updater + + def run(self): + meta_casks = self.meta_lookup_service.generate_version_lookup() + + for meta_cask in meta_casks: + depends_on_cask = depends_on_cask_parser.parse(meta_cask) + meta_file_content = template_service.generate(meta_cask, depends_on_cask) + self.write_meta_cask(meta_cask, meta_file_content) + read_me_updater.update(meta_cask, depends_on_cask) + + def write_meta_cask(self, meta_cask: MetaCask, content: str): + target_path = "{}.rb".format(os.path.join(self.cask_directory, meta_cask.meta_cask_name)) + with open(target_path, "w") as file: + file.write(content) + + +if __name__ == '__main__': + cask_directory = os.path.join(os.path.dirname(__file__), "Casks") + + meta_lookup_service = MetaLookupService(cask_directory) + depends_on_cask_parser = DependsOnCaskParser(cask_directory) + template_service = TemplateService() + read_me_updater = ReadMeUpdater() + + meta_updater = MetaUpdater(cask_directory, meta_lookup_service, depends_on_cask_parser, template_service, read_me_updater) + meta_updater.run() + + parser = argparse.ArgumentParser() + parser.add_argument("--really_commit", action='store_true', default=False, help='Indicates whether we really commit to git or not') + parser.add_argument("--really_push", action='store_true', default=False, help='Indicates whether we really push to remote or not') + + args = parser.parse_args() + + git_service = GitService() + git_service.push(args.really_commit, args.really_push) + diff --git a/meta-cask.rb.template b/meta-cask.rb.template new file mode 100644 index 000000000..29b2459f7 --- /dev/null +++ b/meta-cask.rb.template @@ -0,0 +1,16 @@ +cask "{cask_name}" do + arch arm: "arm64", intel: "x64" + + version "{sdk_version},{runtime_version}" + sha256 :no_check + + url "https://github.com/isen-ng/homebrew-dotnet-sdk-versions/raw/master/META.md" + name ".NET Core SDK #{version.csv.first}" + desc "This cask follows releases from https://github.com/dotnet/core/tree/master" + homepage "https://github.com/isen-ng/homebrew-dotnet-sdk-versions" + + depends_on cask: "{depends_on_cask}" + depends_on macos: "{depends_on_macos}" + + stage_only true +end diff --git a/meta-cask.x64.rb.template b/meta-cask.x64.rb.template new file mode 100644 index 000000000..f6f6f85d0 --- /dev/null +++ b/meta-cask.x64.rb.template @@ -0,0 +1,14 @@ +cask "{cask_name}" do + version "{sdk_version},{runtime_version}" + sha256 :no_check + + url "https://github.com/isen-ng/homebrew-dotnet-sdk-versions/raw/master/META.md" + name ".NET Core SDK #{version.csv.first}" + desc "This cask follows releases from https://github.com/dotnet/core/tree/master" + homepage "https://github.com/isen-ng/homebrew-dotnet-sdk-versions" + + depends_on cask: "{depends_on_cask}" + depends_on macos: "{depends_on_macos}" + + stage_only true +end diff --git a/meta_selector.py b/meta_selector.py deleted file mode 100644 index a1f343487..000000000 --- a/meta_selector.py +++ /dev/null @@ -1,209 +0,0 @@ -import os -import re -from typing import List, Dict -import subprocess -from urllib import request -from hashlib import sha256 - - -class Sdk: - def __init__(self, filename: str, name: str, major: str, minor: str, patch): - self.filename = filename - self.name = name - self.major = major - self.minor = minor - self.patch = patch - self.fullname = filename.replace(".rb", ""); - - def is_newer_than(self, other): - if self.major > other.major: - return True - if self.minor > other.minor: - return True - if self.patch > other.patch: - return True - return False - - -class MetaSelector: - quiet = False - - __keep_line_matchers = [ - re.compile("^\\s*arch\\b.*"), - re.compile("^\\s*version\\b.*"), - re.compile("^\\s*name\\b.*"), - re.compile("^\\s*desc\\b.*"), - re.compile("^\\s*depends_on\\b.*") - ] - - __url_vars = [ - "url", - "url_x64", - "url_arm64" - ] - - __web_url_regex = re.compile(r".*://(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)") - __git_url_regex = re.compile(r".*@(?P[a-zA-Z0-9_.-]+):(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)") - __git_ssh_regex = re.compile(r".*://.*@(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)/(?P[a-zA-Z0-9_.-]+)") - __version_regex = re.compile(r"(?Pdotnet-sdk)(?P\d+)-(?P\d+)-(?P\d+)") - - def __init__(self, workdir): - self.workdir = workdir - pass - - def run(self): - files = self.list_work_files(self.workdir) - lookup = self.generate_version_lookup(files) - for key in lookup: - sdk = lookup[key] - self.create_meta_package_from(sdk) - - def create_meta_package_from(self, sdk: Sdk) -> None: - source_path = os.path.join(self.workdir, sdk.filename) - output: List[str] = [ - "cask \"{}\" do".format(sdk.name) - ] - set_url = False - set_cask_dependency = False - [user, repo] = self.read_origin_remote() - github_repo = f"https://github.com/{user}/{repo}" - meta_artifact = f"{github_repo}/raw/master/META.md" - with open(source_path) as fp: - lines = fp.readlines() - assignment_re = re.compile("\\s*(?P[^\\s]+)\\s*=?\\s*(?P.*)") - last_var = "" - for line in lines: - line = line.rstrip() - looks_like_cask_def_start = line.startswith("cask") and line.endswith("do") - match = assignment_re.match(line) - is_var_line = not looks_like_cask_def_start and match is not None - should_store_last_var = False - variable = "" - if is_var_line: - variable = match.group("variable") - is_new_variable = variable != last_var - have_prior_output = len(output) > 1 # should ignore the starting line too - last_line_is_blank = output[-1] == "" - if is_new_variable and have_prior_output and not last_line_is_blank: - if last_var not in ["version", "desc", "name"] and last_var not in self.__url_vars: - output.append("") - - if variable in self.__url_vars and set_url is False: - output.append(f" url \"{meta_artifact}\"") - set_url = True - should_store_last_var = True - elif variable == "depends_on": - if not set_cask_dependency: - output.append(f" depends_on cask: \"{sdk.fullname}\"") - should_store_last_var = True - elif variable == "homepage": - output.append(f" homepage \"{github_repo}\"") - should_store_last_var = True - - if self.should_keep_line(line): - output.append(line) - should_store_last_var = True - if variable == "version": - output.append(f" sha256 :no_check") - output.append("") - if should_store_last_var: - last_var = variable - elif variable in self.__url_vars: - last_var = "url" - # the meta package contains no activatable artifacts - # -> according to https://github.com/krema/homebrew-cask-local/blob/master/doc/cask_language_reference/all_stanzas.md - # we can set stage_only: true - # otherwise the verification workflows will fail with "at least one activatable artifact stanza is required" - output.append(" stage_only true") - output.append("end") - - self.log("creating meta-package: {}".format(source_path)) - target_path = "{}.rb".format(os.path.join(self.workdir, sdk.name)) - with open(target_path, "w") as fp: - fp.writelines(f"{s}\n" for s in output) - - def hash(self, url: str) -> str: - req = request.urlopen(url) - raw_data = req.read() - result = sha256(raw_data) - return result.hexdigest() - - def read_origin_remote(self) -> List[str]: - lines = [str(l) for l in subprocess.check_output("git remote -v", shell=True).decode("utf-8").splitlines()] - origin_lines = [l for l in lines if l.strip().startswith("origin")] - if len(origin_lines) == 0: - print(lines) - print(origin_lines) - raise Exception("Unable to determine url for remote: origin") - parts = origin_lines[0].split() - if parts[0] != "origin": - raise Exception( - f"Danger, Will Robinson: first non-whitespace part expected to be 'origin' for line {origin_lines[0]}") - return self.parse_origin_remote(parts[1]) - - def parse_origin_remote(self, line: str) -> List[str]: - web_match = self.__web_url_regex.match(line) - if web_match is not None: - user = web_match.group("user") - repo = web_match.group("repo") - return [user, repo] - - git_match = self.__git_url_regex.match(line) - if git_match is not None: - user = git_match.group("user") - repo = git_match.group("repo").replace(".git", "") - return [user, repo] - - ssh_match = self.__git_ssh_regex.match(line) - if ssh_match is not None: - user = ssh_match.group("user") - repo = ssh_match.group("repo").replace(".git", "") - return [user, repo] - - raise Exception("Unable to parse origin url: {}".format(line)) - - def log(self, s: str) -> None: - if self.quiet: - return - print(s) - - def generate_version_lookup(self, files) -> Dict[str, Sdk]: - lookup = {} - for f in files: - match = self.__version_regex.match(f) - if match is None: - print("ignored {}: No version match match for filename".format(f)) - continue - - major = match.group("major") - name = "{}{}".format(match.group("name"), major) - minor = match.group("minor") - patch = match.group("patch") - current = Sdk(f, name, major, minor, patch) - - if name not in lookup: - lookup[name] = current - continue - - existing = lookup[name] - if current.is_newer_than(existing): - lookup[name] = current - return lookup - - @staticmethod - def list_work_files(workdir): - return [ - f for f in os.listdir(workdir) - if os.path.isfile(os.path.join(workdir, f)) - ] - - def should_keep_line(self, line: str): - for matcher in self.__keep_line_matchers: - if matcher.match(line) is not None: - return True - return False - - -if __name__ == '__main__': - workdir = os.path.join(os.path.dirname(__file__), "Casks") - MetaSelector(workdir).run() diff --git a/test_meta_selector.py b/test_meta_selector.py deleted file mode 100644 index 77182a269..000000000 --- a/test_meta_selector.py +++ /dev/null @@ -1,333 +0,0 @@ -import unittest -from meta_selector import MetaSelector -import tempfile -import os -import shutil -import subprocess -from typing import List, Dict - - -class MetaSelectorTests(unittest.TestCase): - _workdir = None - - @classmethod - def setUpClass(cls): - cls._workdir = tempfile.mkdtemp() - - @classmethod - def tearDownClass(cls): - shutil.rmtree(cls._workdir) - - def test_run_should_select_highest_version_for_single_sdk_two_files(self): - self.assertFalse(self._workdir is None) - self.create_caskfile("7-0-200") - expected_version = "7-0-400" - self.create_caskfile(expected_version) - expected_file = "dotnet-sdk7.rb" - sut = self.create() - - sut.run() - - self.assertWorkFileExists(expected_file) - # file name already matched - the cask definition should too - full_path = self.work_file(expected_file) - with open(full_path, "r") as fp: - lines = fp.readlines() - interesting = [l for l in lines if l.startswith("cask")] - self.assertEqual(1, len(interesting)) - self.assertEqual("cask \"dotnet-sdk7\" do", interesting[0].rstrip()) - - def test_run_should_set_url_to_meta_readme_from_current_repository(self): - # TODO: determine the location via `git remote -v`, observing the origin - # then test that this is what the script uses as the base url to META.md - stdout = (subprocess.run( - ["git", "remote", "-v"], - stdout=subprocess.PIPE - ).stdout.decode("utf-8")).splitlines() - interesting = [l for l in stdout if l.startswith("origin")] - expectedUrl = "https://github.com/isen-ng/homebrew-dotnet-sdk-versions/raw/master/META.md" - - self.create_caskfile("7-0-200") - expected_version = "7-0-400" - self.create_caskfile(expected_version) - expected_file = "dotnet-sdk7.rb" - sut = self.create() - - sut.run() - - lines = self.read_work_file(expected_file) - interesting = [l.strip() for l in lines if l.strip().startswith("url")] - self.assertEqual(1, len(interesting)) - self.assertEqual(f"url \"{expectedUrl}\"", interesting[0]) - - def test_run_should_add_dotnet_dependency(self): - self.create_caskfile("7-0-200") - expected_version = "7-0-400" - self.create_caskfile(expected_version) - expected_file = "dotnet-sdk7.rb" - sut = self.create() - - sut.run() - - lines = self.read_work_file(expected_file) - interesting = [l.strip() for l in lines if - l.strip().startswith("depends_on") and - l.find("cask") > -1] - self.assertEqual(1, len(interesting)) - self.assertEqual( - "depends_on cask: \"dotnet-sdk7-0-400\"", - interesting[0] - ) - - def test_run_should_append_non_variable_lines(self): - self.create_caskfile("7-0-200") - expected_version = "7-0-400" - self.create_caskfile(expected_version) - expected_file = "dotnet-sdk7.rb" - sut = self.create() - - sut.run() - - raw = self.read_work_file_raw(expected_file) - self.assertContains(raw, "name \".NET Core SDK #{version.csv.first}\"") - self.assertContains(raw, "desc \"This cask follows releases from https://github.com/dotnet/core/tree/master\"") - self.assertContains(raw, "homepage \"https://github.com/isen-ng/homebrew-dotnet-sdk-versions\"") - - def test_run_should_not_duplicate_depends_on(self): - self.create_caskfile("7-0-200") - expected_version = "7-0-400" - self.create_caskfile(expected_version) - expected_file = "dotnet-sdk7.rb" - sut = self.create() - - sut.run() - - lines = self.read_work_file(expected_file) - depends_on_lines = [ l for l in lines if l.find("depends_on") > -1 ] - macos_depends = [ l for l in depends_on_lines if l.find("macos") > -1] - self.assertEqual(1, len(macos_depends)) - - def test_parse_origin_remote_ssh(self): - sut = self.create() - url = "ssh://git@github.com/owner/repo" - - result = sut.parse_origin_remote(url) - - self.assertEqual(result[0], "owner") - self.assertEqual(result[1], "repo") - - def assertContains(self, actual, expected): - self.assertTrue(actual.find(expected) > -1) - - def create(self): - sut = MetaSelector(self._workdir) - sut.quiet = True - return sut - - def work_file(self, relative_path: str): - return os.path.join(self._workdir, relative_path); - - def read_work_file(self, relative_path: str) -> List[str]: - self.assertWorkFileExists(relative_path) - full_path = self.work_file(relative_path) - with open(full_path, "r") as fp: - return fp.readlines() - - def read_work_file_raw(self, relative_path: str) -> str: - self.assertWorkFileExists(relative_path) - full_path = self.work_file(relative_path) - with open(full_path, "r") as fp: - return fp.read() - - def assertWorkFileExists(self, name: str): - full_path = self.work_file(name) - if os.path.exists(full_path): - return - self.fail( - "expected to find file\n{}\nin\n{}".format(name, self._workdir) - ) - - def create_caskfile(self, version: str): - filename = "dotnet-sdk{}.rb".format(version) - with open(os.path.join(self._workdir, filename), mode="w") as fp: - fp.write( - self._definitions[version] - ) - - # the final files should look something like this, which - # was hand-crafted very similarly on a mac and worked there - # to install the README.md from the repo (because META.md isn't - # there yet - that's also going to be fun to fix) and the - # mentioned dotnet sdk dependency - __example__ = """ -cask "dotnet-sdk7" do # name should be generated by the meta_selector tool - arch arm: "arm64", intel: "x64" # * - version "7.0.405,7.0.15" # * - - # TODO: this must still be added by meta_selector - url "https://github.com/isen-ng/homebrew-dotnet-sdk-versions/blob/master/META.md" - - name ".NET Core SDK #{version.csv.first}" # * - desc "This cask follows releases from https://github.com/dotnet/core/tree/master" # * - homepage: "https://www.microsoft.com/net/core#macos" # * - - depends_on macos: ">= :mojave" # * - - # TODO: this must still be added by meta_selector - depends_on cask: "dotnet-sdk7-0-400" - - caveats "" # * (and I have to figure out multi-line for this too) -end - """ - - _definitions = { - "6-0-100": """ -cask "dotnet-sdk6-0-100" do - arch arm: "arm64", intel: "x64" - - version "6.0.108,6.0.8" - - sha256_x64 = "c617e8972513c290b3ffccc6063d24d8d1aaadf3cc16c7c369e23bac9f450570" - sha256_arm64 = "d37d779518fb573284176ab55f2b9606f982cf12a4aa9c220bbf6d353ad025b9" - url_x64 = "https://download.visualstudio.microsoft.com/download/pr/528fff70-36c8-4103-87fb-3717512537ad/9a96634944cde13e55e367778246057e/dotnet-sdk-#{version.csv.first}-osx-x64.pkg" - url_arm64 = "https://download.visualstudio.microsoft.com/download/pr/56748cfd-0e22-44b3-a253-018d156b076d/4f669bc1d8355e18c25a2b7b97e57cf6/dotnet-sdk-#{version.csv.first}-osx-arm64.pkg" - - on_arm do - sha256 sha256_arm64 - - url url_arm64 - end - on_intel do - sha256 sha256_x64 - - url url_x64 - end - - name ".NET Core SDK #{version.csv.first}" - desc "This cask follows releases from https://github.com/dotnet/core/tree/master" - homepage "https://www.microsoft.com/net/core#macos" - - livecheck do - skip "See https://github.com/isen-ng/homebrew-dotnet-sdk-versions/blob/master/CONTRIBUTING.md#automatic-updates" - end - - depends_on macos: ">= :mojave" - - pkg "dotnet-sdk-#{version.csv.first}-osx-#{arch}.pkg" - - uninstall pkgutil: "com.microsoft.dotnet.dev.#{version.csv.first}.component.osx.#{arch}" - - zap trash: ["~/.dotnet", "~/.nuget", "/etc/paths.d/dotnet", "/etc/paths.d/dotnet-cli-tools"], - pkgutil: [ - "com.microsoft.dotnet.hostfxr.#{version.csv.second}.component.osx.#{arch}", - "com.microsoft.dotnet.sharedframework.Microsoft.NETCore.App.#{version.csv.second}.component.osx.#{arch}", - "com.microsoft.dotnet.pack.apphost.#{version.csv.second}.component.osx.#{arch}", - "com.microsoft.dotnet.sharedhost.component.osx.#{arch}", - ] - - caveats "Uninstalling the offical dotnet-sdk casks will remove the shared runtime dependencies, " \ - "so you'll need to reinstall the particular version cask you want from this tap again " \ - "for the `dotnet` command to work again." -end - """.strip(), - - "7-0-200": """ -cask "dotnet-sdk7-0-200" do - arch arm: "arm64", intel: "x64" - - version "7.0.203,7.0.5" - - sha256_x64 = "82e6bfd3301d1b718e136fdaa1dd23d6abb03adbca05fcbc268b0da7e0d65b46" - sha256_arm64 = "c80e0eabfa3681fa1db33c149ccbbb7de9155702aa5baaa11ea63f998151f326" - url_x64 = "https://download.visualstudio.microsoft.com/download/pr/08b3b509-05e6-4df1-84e1-a76c8855d899/d9b9a2d8ef9788f345d97304ceb67b07/dotnet-sdk-#{version.csv.first}-osx-x64.pkg" - url_arm64 = "https://download.visualstudio.microsoft.com/download/pr/a5070179-d53b-4a84-98d6-37a9d3ef458b/f8bba83817d23e3b7726746c59de4e0c/dotnet-sdk-#{version.csv.first}-osx-arm64.pkg" - - on_arm do - sha256 sha256_arm64 - - url url_arm64 - end - on_intel do - sha256 sha256_x64 - - url url_x64 - end - - name ".NET Core SDK #{version.csv.first}" - desc "This cask follows releases from https://github.com/dotnet/core/tree/master" - homepage "https://www.microsoft.com/net/core#macos" - - livecheck do - skip "See https://github.com/isen-ng/homebrew-dotnet-sdk-versions/blob/master/CONTRIBUTING.md#automatic-updates" - end - - depends_on macos: ">= :mojave" - - pkg "dotnet-sdk-#{version.csv.first}-osx-#{arch}.pkg" - - uninstall pkgutil: "com.microsoft.dotnet.dev.#{version.csv.first}.component.osx.#{arch}" - - zap trash: ["~/.dotnet", "~/.nuget", "/etc/paths.d/dotnet", "/etc/paths.d/dotnet-cli-tools"], - pkgutil: [ - "com.microsoft.dotnet.hostfxr.#{version.csv.second}.component.osx.#{arch}", - "com.microsoft.dotnet.sharedframework.Microsoft.NETCore.App.#{version.csv.second}.component.osx.#{arch}", - "com.microsoft.dotnet.pack.apphost.#{version.csv.second}.component.osx.#{arch}", - "com.microsoft.dotnet.sharedhost.component.osx.#{arch}", - ] - - caveats "Uninstalling the offical dotnet-sdk casks will remove the shared runtime dependencies, " \ - "so you'll need to reinstall the particular version cask you want from this tap again " \ - "for the `dotnet` command to work again." -end - """.strip(), - - "7-0-400": """ -cask "dotnet-sdk7-0-200" do - arch arm: "arm64", intel: "x64" - - version "7.0.203,7.0.5" - - sha256_x64 = "82e6bfd3301d1b718e136fdaa1dd23d6abb03adbca05fcbc268b0da7e0d65b46" - sha256_arm64 = "c80e0eabfa3681fa1db33c149ccbbb7de9155702aa5baaa11ea63f998151f326" - url_x64 = "https://download.visualstudio.microsoft.com/download/pr/08b3b509-05e6-4df1-84e1-a76c8855d899/d9b9a2d8ef9788f345d97304ceb67b07/dotnet-sdk-#{version.csv.first}-osx-x64.pkg" - url_arm64 = "https://download.visualstudio.microsoft.com/download/pr/a5070179-d53b-4a84-98d6-37a9d3ef458b/f8bba83817d23e3b7726746c59de4e0c/dotnet-sdk-#{version.csv.first}-osx-arm64.pkg" - - on_arm do - sha256 sha256_arm64 - - url url_arm64 - end - on_intel do - sha256 sha256_x64 - - url url_x64 - end - - name ".NET Core SDK #{version.csv.first}" - desc "This cask follows releases from https://github.com/dotnet/core/tree/master" - homepage "https://www.microsoft.com/net/core#macos" - - livecheck do - skip "See https://github.com/isen-ng/homebrew-dotnet-sdk-versions/blob/master/CONTRIBUTING.md#automatic-updates" - end - - depends_on macos: ">= :mojave" - - pkg "dotnet-sdk-#{version.csv.first}-osx-#{arch}.pkg" - - uninstall pkgutil: "com.microsoft.dotnet.dev.#{version.csv.first}.component.osx.#{arch}" - - zap trash: ["~/.dotnet", "~/.nuget", "/etc/paths.d/dotnet", "/etc/paths.d/dotnet-cli-tools"], - pkgutil: [ - "com.microsoft.dotnet.hostfxr.#{version.csv.second}.component.osx.#{arch}", - "com.microsoft.dotnet.sharedframework.Microsoft.NETCore.App.#{version.csv.second}.component.osx.#{arch}", - "com.microsoft.dotnet.pack.apphost.#{version.csv.second}.component.osx.#{arch}", - "com.microsoft.dotnet.sharedhost.component.osx.#{arch}", - ] - - caveats "Uninstalling the offical dotnet-sdk casks will remove the shared runtime dependencies, " \ - "so you'll need to reinstall the particular version cask you want from this tap again " \ - "for the `dotnet` command to work again." -end - """.strip() - }