From 9671f9ce96a8cd26b85977e12ff913ea7f7653b9 Mon Sep 17 00:00:00 2001 From: Chen Kasirer Date: Mon, 3 Aug 2026 15:16:22 +0200 Subject: [PATCH 1/6] long due addition of pythonnet to requirements --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c0e6e6c..4a2527d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ tomlkit -semver \ No newline at end of file +semver +pythonnet \ No newline at end of file From 1a372ad01115aba8164a0635f1a4b2e0db39b4bf Mon Sep 17 00:00:00 2001 From: Chen Kasirer Date: Mon, 3 Aug 2026 15:16:45 +0200 Subject: [PATCH 2/6] added support for macos in the yakerize task --- src/compas_invocations2/grasshopper.py | 68 +++++++++++++++++++++----- 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/src/compas_invocations2/grasshopper.py b/src/compas_invocations2/grasshopper.py index 2cf9fa0..a7bc74e 100644 --- a/src/compas_invocations2/grasshopper.py +++ b/src/compas_invocations2/grasshopper.py @@ -7,11 +7,14 @@ """ import os +import platform import re import shutil +import subprocess import tempfile from pathlib import Path from typing import List +from typing import Optional import invoke import requests @@ -21,18 +24,57 @@ YAK_URL = r"https://files.mcneel.com/yak/tools/latest/yak.exe" +# The `yak` CLI shipped inside the Rhino application bundle on macOS. +RHINO_YAK_PATHS = [ + "/Applications/Rhino 8.app/Contents/Resources/bin/yak", + "/Applications/Rhino 7.app/Contents/Resources/bin/yak", +] + def _download_yak_executable(target_dir: str): response = requests.get(YAK_URL) if response.status_code != 200: raise ValueError(f"Failed to download the yak.exe from url:{YAK_URL} with error : {response.status_code}") - target_path = os.path.join(target_dir, "yak.exe") + # absolute, because callers run yak from inside a different working directory + target_path = os.path.abspath(os.path.join(target_dir, "yak.exe")) with open(target_path, "wb") as f: f.write(response.content) return target_path +def _find_native_yak() -> Optional[str]: + """Return the path to a natively runnable ``yak`` CLI, if one is installed.""" + for path in RHINO_YAK_PATHS: + if os.path.isfile(path) and os.access(path, os.X_OK): + return path + return shutil.which("yak") + + +def _get_yak_command(download_dir: str) -> List[str]: + """Return the argv prefix used to invoke yak, downloading ``yak.exe`` if needed. + + The only yak binary McNeel publishes for download is a .NET Framework ``yak.exe``. + On Windows it runs as-is. Elsewhere it needs a runtime, so we prefer the native + ``yak`` CLI that ships inside the Rhino application bundle (macOS) or is otherwise + on PATH, and fall back to running the downloaded ``yak.exe`` under Mono. + """ + if platform.system() == "Windows": + return [_download_yak_executable(download_dir)] + + native_yak = _find_native_yak() + if native_yak: + return [native_yak] + + mono = shutil.which("mono") + if not mono: + raise invoke.Exit( + "No yak executable available. Install Rhino (which bundles the `yak` CLI) " + "or install Mono (`brew install mono`) so that the downloaded yak.exe can be run." + ) + return [mono, _download_yak_executable(download_dir)] + + def _set_version_in_manifest(manifest_path: str, version: str): with open(manifest_path, "r") as f: lines = f.readlines() @@ -202,17 +244,15 @@ def yakerize( # yak executable shouldn't be in the target directory, otherwise it will be included in the package target_parent = os.sep.join(target_dir.split(os.sep)[:-1]) try: - yak_exe_path = _download_yak_executable(target_parent) + yak_cmd = _get_yak_command(target_parent) except ValueError: raise invoke.Exit("Failed to download the yak executable") - else: - yak_exe_path = os.path.abspath(yak_exe_path) with chdir(target_dir): try: # not using `ctx.run()` here to get properly formatted output (unicode+colors) - os.system(f"{yak_exe_path} build --platform any") - except Exception as e: + subprocess.run(yak_cmd + ["build", "--platform", "any"], check=True) + except (OSError, subprocess.CalledProcessError) as e: raise invoke.Exit(f"Failed to build the yak package: {e}") if not any([f.endswith(".yak") for f in os.listdir(target_dir)]): raise invoke.Exit("No .yak file was created in the build directory.") @@ -234,18 +274,24 @@ def publish_yak(ctx, yak_file: str, test_server: bool = False): if not yak_file.endswith(".yak"): raise invoke.Exit("Invalid file type. Must be a .yak file.") + yak_file = os.path.abspath(yak_file) + with chdir(ctx.base_folder): with tempfile.TemporaryDirectory("actions.publish_yak") as action_dir: try: - _download_yak_executable(action_dir) + yak_cmd = _get_yak_command(action_dir) except ValueError: raise invoke.Exit("Failed to download the yak executable") - yak_exe_path: str = os.path.join(action_dir, "yak.exe") + cmd = yak_cmd + ["push"] if test_server: - ctx.run(f"{yak_exe_path} push --source https://test.yak.rhino3d.com {yak_file}") - else: - ctx.run(f"{yak_exe_path} push {yak_file}") + cmd += ["--source", "https://test.yak.rhino3d.com"] + cmd.append(yak_file) + + try: + subprocess.run(cmd, check=True) + except (OSError, subprocess.CalledProcessError) as e: + raise invoke.Exit(f"Failed to publish the yak package: {e}") def _is_header_line(line: str) -> bool: From 50c278058acbc8b9c9ad919d9df9fc2b2f842a0c Mon Sep 17 00:00:00 2001 From: Chen Kasirer Date: Mon, 3 Aug 2026 15:31:03 +0200 Subject: [PATCH 3/6] yak paths configurable in context --- src/compas_invocations2/grasshopper.py | 43 +++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/src/compas_invocations2/grasshopper.py b/src/compas_invocations2/grasshopper.py index a7bc74e..c1acc7d 100644 --- a/src/compas_invocations2/grasshopper.py +++ b/src/compas_invocations2/grasshopper.py @@ -172,10 +172,39 @@ def _get_user_object_path(context): return None +def _get_yak_setting(ctx, key: str) -> Optional[str]: + """Return a path configured under the ``yak`` section of the project's tasks.py. + + Relative paths are resolved against ``base_folder``, matching the convention + used for the ``ghuser`` config sections. + """ + if not hasattr(ctx, "yak"): + return None + + value = ctx.yak.get(key) + if not value: + return None + + return value if os.path.isabs(value) else os.path.join(ctx.base_folder, value) + + +def _resolve_yak_path(ctx, key: str, value: Optional[str], description: str) -> str: + """Resolve a path from the task argument, falling back to the ``yak`` config section.""" + path = value or _get_yak_setting(ctx, key) + if not path: + raise invoke.Exit( + f"Please provide the path to the {description}, either using `--{key.replace('_', '-')}` " + f"or by setting `yak.{key}` in the configuration of your tasks.py." + ) + if not os.path.exists(path): + raise invoke.Exit(f"{description.capitalize()} not found at {path}. Please provide a valid path.") + return path + + @invoke.task( help={ - "manifest_path": "Path to the manifest file.", - "logo_path": "Path to the logo file.", + "manifest_path": "(Optional) Path to the manifest file. Defaults to the `yak.manifest_path` setting.", + "logo_path": "(Optional) Path to the logo file. Defaults to the `yak.logo_path` setting.", "gh_components_dir": "(Optional) Path to the directory containing the .ghuser files.", "readme_path": "(Optional) Path to the readme file.", "license_path": "(Optional) Path to the license file.", @@ -185,8 +214,8 @@ def _get_user_object_path(context): ) def yakerize( ctx, - manifest_path: str, - logo_path: str, + manifest_path: str = None, + logo_path: str = None, gh_components_dir: str = None, readme_path: str = None, license_path: str = None, @@ -200,6 +229,9 @@ def yakerize( f"""Invalid target Rhino version `{target_rhino}`. Must be one of: rh6, rh7, rh8. Minor version is optional and can be appended with a '_' (e.g. rh8_15).""" ) + manifest_path = _resolve_yak_path(ctx, "manifest_path", manifest_path, "manifest file") + logo_path = _resolve_yak_path(ctx, "logo_path", logo_path, "logo file") + gh_components_dir = gh_components_dir or _get_user_object_path(ctx) if not gh_components_dir: raise invoke.Exit("Please provide the path to the directory containing the .ghuser files.") @@ -224,7 +256,8 @@ def yakerize( else: os.makedirs(target_dir, exist_ok=False) - manifest_target = shutil.copy(manifest_path, target_dir) + # yak only recognizes a manifest named `manifest.yml`, regardless of the source filename + manifest_target = shutil.copy(manifest_path, os.path.join(target_dir, "manifest.yml")) _set_version_in_manifest(manifest_target, version) shutil.copy(logo_path, target_dir) From 95215b3731a30e2a48c1820e2a7573ff487cc0fa Mon Sep 17 00:00:00 2001 From: Chen Kasirer Date: Mon, 3 Aug 2026 15:31:59 +0200 Subject: [PATCH 4/6] updated changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ac9f86..599fbbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Added support for macOS in `invoke yakerize`. +* Added support for yakerize configuration through the context configuration dictionary. + ### Changed ### Removed From 15b8cf6cadda2806f26b54d9ad1757b22f755903 Mon Sep 17 00:00:00 2001 From: Chen Kasirer Date: Mon, 3 Aug 2026 16:38:33 +0200 Subject: [PATCH 5/6] added rhino9 --- src/compas_invocations2/grasshopper.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/compas_invocations2/grasshopper.py b/src/compas_invocations2/grasshopper.py index c1acc7d..7be7a2f 100644 --- a/src/compas_invocations2/grasshopper.py +++ b/src/compas_invocations2/grasshopper.py @@ -26,6 +26,7 @@ # The `yak` CLI shipped inside the Rhino application bundle on macOS. RHINO_YAK_PATHS = [ + "/Applications/Rhino 9.app/Contents/Resources/bin/yak", "/Applications/Rhino 8.app/Contents/Resources/bin/yak", "/Applications/Rhino 7.app/Contents/Resources/bin/yak", ] @@ -69,8 +70,7 @@ def _get_yak_command(download_dir: str) -> List[str]: mono = shutil.which("mono") if not mono: raise invoke.Exit( - "No yak executable available. Install Rhino (which bundles the `yak` CLI) " - "or install Mono (`brew install mono`) so that the downloaded yak.exe can be run." + "No yak executable available. Install Rhino (which bundles the `yak` CLI) or install Mono (`brew install mono`) so that the downloaded yak.exe can be run." ) return [mono, _download_yak_executable(download_dir)] @@ -193,8 +193,7 @@ def _resolve_yak_path(ctx, key: str, value: Optional[str], description: str) -> path = value or _get_yak_setting(ctx, key) if not path: raise invoke.Exit( - f"Please provide the path to the {description}, either using `--{key.replace('_', '-')}` " - f"or by setting `yak.{key}` in the configuration of your tasks.py." + f"Please provide the path to the {description}, either using `--{key.replace('_', '-')}` or by setting `yak.{key}` in the configuration of your tasks.py." ) if not os.path.exists(path): raise invoke.Exit(f"{description.capitalize()} not found at {path}. Please provide a valid path.") @@ -296,9 +295,7 @@ def yakerize( os.rename(taget_file, new_filename) -@invoke.task( - help={"yak_file": "Path to the .yak file to publish.", "test_server": "True to publish to the test server."} -) +@invoke.task(help={"yak_file": "Path to the .yak file to publish.", "test_server": "True to publish to the test server."}) def publish_yak(ctx, yak_file: str, test_server: bool = False): """Publish a YAK package to the YAK server.""" From 2dfee09bfbe74ec6612433bb9b3de428a0cff174 Mon Sep 17 00:00:00 2001 From: Chen Kasirer Date: Mon, 3 Aug 2026 17:23:58 +0200 Subject: [PATCH 6/6] fixed linting issues --- src/compas_invocations2/grasshopper.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/compas_invocations2/grasshopper.py b/src/compas_invocations2/grasshopper.py index 7be7a2f..98f18b3 100644 --- a/src/compas_invocations2/grasshopper.py +++ b/src/compas_invocations2/grasshopper.py @@ -70,7 +70,8 @@ def _get_yak_command(download_dir: str) -> List[str]: mono = shutil.which("mono") if not mono: raise invoke.Exit( - "No yak executable available. Install Rhino (which bundles the `yak` CLI) or install Mono (`brew install mono`) so that the downloaded yak.exe can be run." + "No yak executable available. Install Rhino (which bundles the `yak` CLI) " + "or install Mono (`brew install mono`) so that the downloaded yak.exe can be run." ) return [mono, _download_yak_executable(download_dir)] @@ -193,7 +194,8 @@ def _resolve_yak_path(ctx, key: str, value: Optional[str], description: str) -> path = value or _get_yak_setting(ctx, key) if not path: raise invoke.Exit( - f"Please provide the path to the {description}, either using `--{key.replace('_', '-')}` or by setting `yak.{key}` in the configuration of your tasks.py." + f"""Please provide the path to the {description}, either using `--{key.replace("_", "-")}` + or by setting `yak.{key}` in the configuration of your tasks.py.""" ) if not os.path.exists(path): raise invoke.Exit(f"{description.capitalize()} not found at {path}. Please provide a valid path.") @@ -295,7 +297,9 @@ def yakerize( os.rename(taget_file, new_filename) -@invoke.task(help={"yak_file": "Path to the .yak file to publish.", "test_server": "True to publish to the test server."}) +@invoke.task( + help={"yak_file": "Path to the .yak file to publish.", "test_server": "True to publish to the test server."} +) def publish_yak(ctx, yak_file: str, test_server: bool = False): """Publish a YAK package to the YAK server."""