Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
tomlkit
semver
semver
pythonnet
112 changes: 96 additions & 16 deletions src/compas_invocations2/grasshopper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,18 +24,58 @@

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 9.app/Contents/Resources/bin/yak",
"/Applications/Rhino 8.app/Contents/Resources/bin/yak",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe add rhino 9 as well, since it's already in beta

"/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()
Expand Down Expand Up @@ -130,10 +173,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("_", "-")}`
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.",
Expand All @@ -143,8 +215,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,
Expand All @@ -158,6 +230,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.")
Expand All @@ -182,7 +257,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)

Expand All @@ -202,17 +278,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.")
Expand All @@ -234,18 +308,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:
Expand Down
Loading