You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tsfile.apache.org by co...@apache.org on 2026/09/17 06:30:59 UTC
(tsfile) 16/24: feat(ci): build Homebrew development bottles
This is an automated email from the ASF dual-hosted git repository.
ColinLeeo pushed a commit to branch feat/native-package-artifacts
in repository https://gitbox.apache.org/repos/asf/tsfile.git
commit 8b6966a76da397269b22d09e4628de5c611e59ab
Author: ColinLee <sh...@163.com>
AuthorDate: Thu Sep 10 19:19:17 2026 +0800
feat(ci): build Homebrew development bottles
---
.github/workflows/native-packages.yml | 183 ++++++++++++++++++++++++
packaging/README.md | 24 ++++
packaging/homebrew/tsfile-dev.rb.in | 66 +++++++++
packaging/scripts/render_homebrew_formula.py | 83 +++++++++++
packaging/tests/test_render_homebrew_formula.py | 177 +++++++++++++++++++++++
5 files changed, 533 insertions(+)
diff --git a/.github/workflows/native-packages.yml b/.github/workflows/native-packages.yml
index fdc560e3a..019918a70 100644
--- a/.github/workflows/native-packages.yml
+++ b/.github/workflows/native-packages.yml
@@ -476,3 +476,186 @@ jobs:
path: packages/*.zip
if-no-files-found: error
retention-days: 14
+
+ build-homebrew:
+ name: Build Homebrew bottle on ${{ matrix.name }}
+ needs: prepare
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 45
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: macos-arm64
+ os: macos-latest
+ - name: macos-x86_64
+ os: macos-15-intel
+ env:
+ HOMEBREW_NO_AUTO_UPDATE: "1"
+ HOMEBREW_NO_INSTALL_CLEANUP: "1"
+ TSFILE_HOMEBREW_VERSION: ${{ needs.prepare.outputs.homebrew_version }}
+ SOURCE_REPOSITORY: ColinLeeo/tsfile
+ SOURCE_GIT_SHA: ${{ github.sha }}
+ BOTTLE_ROOT_URL: https://packages.apache.org/artifactory/tsfile/homebrew/dev/versions/${{ needs.prepare.outputs.homebrew_version }}/bottles
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v7
+
+ - name: Render immutable source Formula in a temporary local tap
+ shell: bash
+ run: |
+ mkdir -p homebrew/Formula homebrew/bottles
+ curl --fail --location --retry 3 \
+ "https://github.com/$SOURCE_REPOSITORY/archive/$SOURCE_GIT_SHA.tar.gz" \
+ --output "$RUNNER_TEMP/tsfile-source.tar.gz"
+ source_sha256="$(shasum -a 256 "$RUNNER_TEMP/tsfile-source.tar.gz" | awk '{print $1}')"
+ python3 packaging/scripts/render_homebrew_formula.py \
+ --template packaging/homebrew/tsfile-dev.rb.in \
+ --repository "$SOURCE_REPOSITORY" \
+ --git-sha "$SOURCE_GIT_SHA" \
+ --version "$TSFILE_HOMEBREW_VERSION" \
+ --source-sha256 "$source_sha256" \
+ --output homebrew/Formula/tsfile-dev.rb
+ ruby -c homebrew/Formula/tsfile-dev.rb
+ brew tap-new apache/tsfile-dev
+ tap_path="$(brew --repository apache/tsfile-dev)"
+ cp homebrew/Formula/tsfile-dev.rb "$tap_path/Formula/tsfile-dev.rb"
+
+ - name: Build, test, and bottle the development Formula
+ shell: bash
+ run: |
+ brew install --build-bottle apache/tsfile-dev/tsfile-dev
+ brew test apache/tsfile-dev/tsfile-dev
+ cd homebrew/bottles
+ brew bottle --json --root-url "$BOTTLE_ROOT_URL" apache/tsfile-dev/tsfile-dev
+ shopt -s nullglob
+ bottles=(*.bottle*.tar.gz)
+ metadata=(*.bottle.json)
+ [[ ${#bottles[@]} -eq 1 && ${#metadata[@]} -eq 1 ]]
+
+ - name: Upload platform Homebrew bottle and source Formula
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-homebrew-${{ matrix.name }}
+ path: homebrew/
+ if-no-files-found: error
+ retention-days: 14
+
+ merge-homebrew:
+ name: Merge Homebrew development bottles
+ needs: [prepare, build-homebrew]
+ runs-on: macos-latest
+ timeout-minutes: 15
+ env:
+ HOMEBREW_NO_AUTO_UPDATE: "1"
+ TSFILE_HOMEBREW_VERSION: ${{ needs.prepare.outputs.homebrew_version }}
+ BOTTLE_ROOT_URL: https://packages.apache.org/artifactory/tsfile/homebrew/dev/versions/${{ needs.prepare.outputs.homebrew_version }}/bottles
+ steps:
+ - name: Download both platform Homebrew artifacts separately
+ uses: actions/download-artifact@v8
+ with:
+ pattern: native-homebrew-macos-*
+ path: homebrew-intermediate
+ merge-multiple: false
+
+ - name: Recreate the same pre-bottle Formula and collect metadata
+ shell: bash
+ run: |
+ arm=homebrew-intermediate/native-homebrew-macos-arm64
+ intel=homebrew-intermediate/native-homebrew-macos-x86_64
+ cmp "$arm/Formula/tsfile-dev.rb" "$intel/Formula/tsfile-dev.rb"
+ brew tap-new apache/tsfile-dev
+ tap_path="$(brew --repository apache/tsfile-dev)"
+ cp "$arm/Formula/tsfile-dev.rb" "$tap_path/Formula/tsfile-dev.rb"
+ mkdir -p homebrew/Formula homebrew/bottles
+ python3 - <<'PY'
+ import hashlib
+ import json
+ import os
+ import shutil
+ from pathlib import Path
+
+ destination = Path("homebrew/bottles")
+ tags = set()
+ for platform in ("macos-arm64", "macos-x86_64"):
+ directory = Path("homebrew-intermediate") / f"native-homebrew-{platform}" / "bottles"
+ metadata = list(directory.glob("*.bottle.json"))
+ assert len(metadata) == 1, f"Expected one JSON for {platform}"
+ data = json.loads(metadata[0].read_text())
+ assert list(data) == ["apache/tsfile-dev/tsfile-dev"]
+ entry = data["apache/tsfile-dev/tsfile-dev"]
+ assert entry["formula"]["pkg_version"] == os.environ["TSFILE_HOMEBREW_VERSION"]
+ assert entry["formula"]["path"] == "Library/Taps/apache/homebrew-tsfile-dev/Formula/tsfile-dev.rb"
+ assert entry["bottle"]["root_url"] == os.environ["BOTTLE_ROOT_URL"]
+ assert len(entry["bottle"]["tags"]) == 1
+ tag, bottle = next(iter(entry["bottle"]["tags"].items()))
+ assert (platform == "macos-arm64") == tag.startswith("arm64_")
+ assert tag not in tags, f"Duplicate bottle tag: {tag}"
+ tags.add(tag)
+ local_name = bottle["local_filename"]
+ assert Path(local_name).name == local_name
+ source = directory / local_name
+ assert hashlib.sha256(source.read_bytes()).hexdigest() == bottle["sha256"]
+ for source in (source, metadata[0]):
+ target = destination / source.name
+ assert not target.exists(), f"Duplicate artifact: {target}"
+ shutil.copy2(source, target)
+ assert len(tags) == 2
+ PY
+
+ - name: Merge both bottle tags into the Formula
+ shell: bash
+ run: |
+ brew bottle --merge --write --no-commit homebrew/bottles/*.json
+ tap_path="$(brew --repository apache/tsfile-dev)"
+ cp "$tap_path/Formula/tsfile-dev.rb" homebrew/Formula/tsfile-dev.rb
+ ruby -c homebrew/Formula/tsfile-dev.rb
+
+ - name: Verify merged Formula and normalize HTTP bottle filenames
+ shell: bash
+ run: |
+ python3 - <<'PY'
+ import hashlib
+ import json
+ import os
+ import re
+ from pathlib import Path
+ from urllib.parse import unquote
+
+ formula = Path("homebrew/Formula/tsfile-dev.rb").read_text()
+ assert not re.search(r"@[A-Z0-9_]+@", formula), "Unresolved Formula token"
+ assert len(re.findall(r"^\s*bottle\s+do\b", formula, re.M)) == 1
+ assert f'root_url "{os.environ["BOTTLE_ROOT_URL"]}"' in formula
+ directory = Path("homebrew/bottles")
+ metadata = list(directory.glob("*.bottle.json"))
+ assert len(metadata) == 2
+ tags = set()
+ for path in metadata:
+ data = json.loads(path.read_text())
+ entry = data["apache/tsfile-dev/tsfile-dev"]
+ for tag, bottle in entry["bottle"]["tags"].items():
+ assert tag not in tags, f"Duplicate bottle tag: {tag}"
+ tags.add(tag)
+ assert re.search(rf'\b{re.escape(tag)}:\s*"{bottle["sha256"]}"', formula)
+ # Homebrew emits a double-dash local name but serves a single-dash URL.
+ filename = unquote(bottle["filename"])
+ assert Path(filename).name == filename and filename.endswith(".tar.gz")
+ source = directory / bottle["local_filename"]
+ target = directory / filename
+ assert hashlib.sha256(source.read_bytes()).hexdigest() == bottle["sha256"]
+ if source != target:
+ assert not target.exists(), f"Duplicate HTTP bottle name: {filename}"
+ source.rename(target)
+ bottle["local_filename"] = filename
+ path.write_text(json.dumps(data, indent=2) + "\n")
+ assert len(tags) == 2
+ assert len(list(directory.glob("*.tar.gz"))) == 2
+ PY
+
+ - name: Upload merged Homebrew Formula and bottles
+ uses: actions/upload-artifact@v7
+ with:
+ name: native-homebrew
+ path: homebrew/
+ if-no-files-found: error
+ retention-days: 14
diff --git a/packaging/README.md b/packaging/README.md
index 8045a4f11..bf4afaee5 100644
--- a/packaging/README.md
+++ b/packaging/README.md
@@ -94,6 +94,30 @@ brew install ./packaging/homebrew/tsfile.rb
The stable formula URL and checksum must be updated to the ASF source archive
when the first release containing this packaging work is published.
+The manual native-package workflow also builds the self-hosted development
+Formula `tsfile-dev`, using `packaging/homebrew/tsfile-dev.rb.in`. This is not a
+Homebrew/core submission. Each build pins the full workflow commit from
+`ColinLeeo/tsfile`, hashes that source archive, and uses the generated immutable
+Homebrew development version. The Formula retains the CMake install behavior
+and tests the installed C++ consumer and CLI before bottling.
+
+The ARM64 job runs on `macos-latest` and the Intel job on `macos-15-intel`.
+Their `native-homebrew-macos-arm64` and `native-homebrew-macos-x86_64`
+intermediate artifacts remain separate until merge. The merge job checks that
+both source Formula files match, merges both platform JSON files with Homebrew,
+and checks both generated tags and checksums in the resulting Formula.
+
+The `native-homebrew` artifact contains `Formula/tsfile-dev.rb` and `bottles/`
+with both bottle tarballs and both JSON metadata files. When downloaded into
+`homebrew/`, this gives the final `homebrew/Formula/` and `homebrew/bottles/`
+layout. The configured future bottle root is
+`https://packages.apache.org/artifactory/tsfile/homebrew/dev/versions/<homebrew-version>/bottles`.
+Homebrew generates local tarballs with a double dash before the version but
+requests a single dash in HTTP URLs. After merge, the workflow renames each
+tarball to the JSON `filename` (URL-decoded) and updates its `local_filename`
+to match, preserving its checksum. The workflow only uploads GitHub Actions
+artifacts for 14 days; it does not publish to that root or update `latest`.
+
## Windows
The manual workflow builds a 64-bit Release package with Visual Studio 2022 and
diff --git a/packaging/homebrew/tsfile-dev.rb.in b/packaging/homebrew/tsfile-dev.rb.in
new file mode 100644
index 000000000..cc5b60bb2
--- /dev/null
+++ b/packaging/homebrew/tsfile-dev.rb.in
@@ -0,0 +1,66 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+class TsfileDev < Formula
+ desc "Columnar storage library for time series data (development snapshot)"
+ homepage "https://tsfile.apache.org/"
+ url "https://github.com/@SOURCE_REPOSITORY@/archive/@GIT_SHA@.tar.gz"
+ version "@HOMEBREW_VERSION@"
+ sha256 "@SOURCE_SHA256@"
+ license "Apache-2.0"
+
+@BOTTLE_BLOCK@
+
+ depends_on "cmake" => :build
+ depends_on "lz4"
+ depends_on "simde"
+ depends_on "snappy"
+ depends_on "utf8cpp"
+ depends_on "zstd"
+
+ uses_from_macos "zlib"
+
+ def install
+ args = %W[
+ -DBUILD_TEST=OFF
+ -DBUILD_TOOLS=ON
+ -DENABLE_LZMA2=OFF
+ -DTSFILE_DEPENDENCY_SOURCE=AUTO
+ -DTSFILE_ENABLE_NATIVE_ARCH=OFF
+ -DCMAKE_INSTALL_RPATH=#{rpath}
+ ]
+
+ system "cmake", "-S", "cpp", "-B", "build", *args, *std_cmake_args
+ system "cmake", "--build", "build"
+ system "cmake", "--install", "build"
+ end
+
+ test do
+ (testpath / "test.cpp").write <<~CPP
+ #include <tsfile/cwrapper/tsfile_cwrapper.h>
+
+ int main() {
+ return TS_DATATYPE_INT32 == 1 ? 0 : 1;
+ }
+ CPP
+
+ system ENV.cxx, "-std=c++11", "test.cpp", "-I#{include}", "-L#{lib}",
+ "-Wl,-rpath,#{lib}", "-ltsfile", "-o", "test"
+ system "./test"
+ system bin / "tsfile-cli", "--version"
+ end
+end
diff --git a/packaging/scripts/render_homebrew_formula.py b/packaging/scripts/render_homebrew_formula.py
new file mode 100644
index 000000000..a39ba31ee
--- /dev/null
+++ b/packaging/scripts/render_homebrew_formula.py
@@ -0,0 +1,83 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Render the development Formula using an immutable source identity."""
+
+import argparse
+import re
+from pathlib import Path
+
+
+VALUE_PATTERNS = {
+ "SOURCE_REPOSITORY": r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+",
+ "GIT_SHA": r"[0-9a-f]{40}",
+ "HOMEBREW_VERSION": r"[0-9][A-Za-z0-9.]*",
+ "SOURCE_SHA256": r"[0-9a-f]{64}",
+}
+TOKEN = re.compile(r"@([A-Z0-9_]+)@")
+
+
+def render_formula(
+ template: str, values: dict[str, str], bottle_block: str = ""
+) -> str:
+ """Replace declared tokens without interpreting Ruby braces or at-signs."""
+ if values.keys() != VALUE_PATTERNS.keys():
+ raise ValueError(
+ "Formula values must contain exactly the declared source fields"
+ )
+ for name, pattern in VALUE_PATTERNS.items():
+ if not re.fullmatch(pattern, values[name]):
+ raise ValueError(f"Invalid Formula value: {name}")
+ replacements = {**values, "BOTTLE_BLOCK": bottle_block}
+ unknown = set(TOKEN.findall(template)) - replacements.keys()
+ if unknown:
+ raise ValueError(f"Unknown Formula placeholders: {sorted(unknown)}")
+ rendered = TOKEN.sub(lambda match: replacements[match.group(1)], template)
+ if TOKEN.search(rendered):
+ raise ValueError("Unresolved Formula placeholder")
+ if len(re.findall(r"^\s*bottle\s+do\b", rendered, re.MULTILINE)) > 1:
+ raise ValueError("Duplicate bottle blocks")
+ return rendered
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--template", type=Path, required=True)
+ parser.add_argument("--repository", required=True)
+ parser.add_argument("--git-sha", required=True)
+ parser.add_argument("--version", required=True)
+ parser.add_argument("--source-sha256", required=True)
+ parser.add_argument("--bottle-block", type=Path)
+ parser.add_argument("--output", type=Path, required=True)
+ args = parser.parse_args()
+ values = {
+ "SOURCE_REPOSITORY": args.repository,
+ "GIT_SHA": args.git_sha,
+ "HOMEBREW_VERSION": args.version,
+ "SOURCE_SHA256": args.source_sha256,
+ }
+ bottle_block = (
+ args.bottle_block.read_text(encoding="utf-8") if args.bottle_block else ""
+ )
+ args.output.write_text(
+ render_formula(args.template.read_text(encoding="utf-8"), values, bottle_block),
+ encoding="utf-8",
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/packaging/tests/test_render_homebrew_formula.py b/packaging/tests/test_render_homebrew_formula.py
new file mode 100644
index 000000000..0c0047aed
--- /dev/null
+++ b/packaging/tests/test_render_homebrew_formula.py
@@ -0,0 +1,177 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import importlib.util
+import subprocess
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parents[2]
+MODULE_PATH = ROOT / "packaging/scripts/render_homebrew_formula.py"
+TEMPLATE_PATH = ROOT / "packaging/homebrew/tsfile-dev.rb.in"
+VALUES = {
+ "SOURCE_REPOSITORY": "ColinLeeo/tsfile",
+ "GIT_SHA": "abcdef1234567890abcdef1234567890abcdef12",
+ "HOMEBREW_VERSION": "2.5.0.dev0.20260910.123.1.gabcdef1",
+ "SOURCE_SHA256": "0123456789abcdef" * 4,
+}
+TEMPLATE = """class TsfileDev < Formula
+ url "https://github.com/@SOURCE_REPOSITORY@/archive/@GIT_SHA@.tar.gz"
+ version "@HOMEBREW_VERSION@"
+ sha256 "@SOURCE_SHA256@"
+@BOTTLE_BLOCK@
+end
+"""
+BOTTLE_BLOCK = """ bottle do
+ root_url "https://packages.apache.org/artifactory/tsfile/homebrew/dev/versions/2.5.0.dev0.20260910.123.1.gabcdef1/bottles"
+ sha256 cellar: :any, arm64_sequoia: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ sha256 cellar: :any, sequoia: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+ end"""
+
+
+class RenderHomebrewFormulaTest(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.module = None
+ if MODULE_PATH.is_file():
+ spec = importlib.util.spec_from_file_location(
+ "render_homebrew_formula", MODULE_PATH
+ )
+ cls.module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(cls.module)
+
+ def renderer(self):
+ self.assertIsNotNone(self.module, "Homebrew Formula renderer must exist")
+ return self.module.render_formula
+
+ def test_renders_exact_immutable_source_and_both_bottle_tags(self):
+ rendered = self.renderer()(TEMPLATE, VALUES, BOTTLE_BLOCK)
+ self.assertEqual(
+ rendered,
+ """class TsfileDev < Formula
+ url "https://github.com/ColinLeeo/tsfile/archive/abcdef1234567890abcdef1234567890abcdef12.tar.gz"
+ version "2.5.0.dev0.20260910.123.1.gabcdef1"
+ sha256 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+"""
+ + BOTTLE_BLOCK
+ + "\nend\n",
+ )
+
+ def test_pre_bottle_formula_has_no_bottle_block(self):
+ rendered = self.renderer()(TEMPLATE, VALUES)
+ self.assertNotIn("bottle do", rendered)
+ self.assertNotRegex(rendered, r"@[A-Z0-9_]+@")
+
+ def test_rejects_missing_and_undeclared_placeholders(self):
+ render = self.renderer()
+ with self.assertRaises(ValueError):
+ render(
+ TEMPLATE,
+ {key: value for key, value in VALUES.items() if key != "GIT_SHA"},
+ )
+ with self.assertRaises(ValueError):
+ render(TEMPLATE + "@UNKNOWN@", {**VALUES, "UNKNOWN": "anything"})
+
+ def test_rejects_template_tokens_in_bottle_block(self):
+ with self.assertRaises(ValueError):
+ self.renderer()(TEMPLATE, VALUES, BOTTLE_BLOCK.replace("sequoia", "@TAG@"))
+
+ def test_rejects_duplicate_bottle_blocks(self):
+ render = self.renderer()
+ for template, block in (
+ (TEMPLATE, BOTTLE_BLOCK + "\n" + BOTTLE_BLOCK),
+ (TEMPLATE + "\nbottle do\nend\n", BOTTLE_BLOCK),
+ (
+ TEMPLATE.replace("@BOTTLE_BLOCK@", "@BOTTLE_BLOCK@\n@BOTTLE_BLOCK@"),
+ BOTTLE_BLOCK,
+ ),
+ ):
+ with self.subTest(template=template, block=block):
+ with self.assertRaises(ValueError):
+ render(template, VALUES, block)
+
+ def test_rejects_empty_or_non_immutable_source_identity(self):
+ render = self.renderer()
+ for key, value in (
+ ("SOURCE_SHA256", ""),
+ ("SOURCE_SHA256", "bad"),
+ ("GIT_SHA", ""),
+ ("GIT_SHA", "abcdef1"),
+ ("GIT_SHA", "develop"),
+ ("SOURCE_REPOSITORY", 'ColinLeeo/tsfile"'),
+ ("HOMEBREW_VERSION", 'bad"\nversion "injected'),
+ ):
+ with self.subTest(key=key, value=value):
+ with self.assertRaises(ValueError):
+ render(TEMPLATE, {**VALUES, key: value})
+
+ def test_preserves_ruby_interpolation_and_other_at_signs(self):
+ extra = "\n# maintainer@example.org\n# #{rpath}\n"
+ self.assertTrue(self.renderer()(TEMPLATE + extra, VALUES).endswith(extra))
+
+ def test_cli_renders_real_template_with_optional_bottle_file(self):
+ self.renderer()
+ with tempfile.TemporaryDirectory() as temporary_directory:
+ directory = Path(temporary_directory)
+ output = directory / "tsfile-dev.rb"
+ block = directory / "bottle.rb"
+ block.write_text(BOTTLE_BLOCK, encoding="utf-8")
+ command = [
+ sys.executable,
+ str(MODULE_PATH),
+ "--template",
+ str(TEMPLATE_PATH),
+ "--repository",
+ VALUES["SOURCE_REPOSITORY"],
+ "--git-sha",
+ VALUES["GIT_SHA"],
+ "--version",
+ VALUES["HOMEBREW_VERSION"],
+ "--source-sha256",
+ VALUES["SOURCE_SHA256"],
+ "--output",
+ str(output),
+ ]
+ for bottle_args, expected_block in (
+ ([], ""),
+ (["--bottle-block", str(block)], BOTTLE_BLOCK),
+ ):
+ subprocess.run(command + bottle_args, check=True)
+ rendered = output.read_text(encoding="utf-8")
+ self.assertIn("class TsfileDev < Formula", rendered)
+ self.assertIn(
+ 'url "https://github.com/ColinLeeo/tsfile/archive/abcdef1234567890abcdef1234567890abcdef12.tar.gz"',
+ rendered,
+ )
+ self.assertIn('version "2.5.0.dev0.20260910.123.1.gabcdef1"', rendered)
+ self.assertIn(
+ 'sha256 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"',
+ rendered,
+ )
+ self.assertIn("#{rpath}", rendered)
+ self.assertNotRegex(rendered, r"@[A-Z0-9_]+@")
+ if expected_block:
+ self.assertIn(BOTTLE_BLOCK, rendered)
+ else:
+ self.assertNotIn("bottle do", rendered)
+
+
+if __name__ == "__main__":
+ unittest.main()