Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Yakerize #4

Merged
merged 17 commits into from
Jan 28, 2025
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

* Added task `yakerize` to create YAK package for Grasshopper.

### Changed

* Task `build-cpython-ghuser-components` now uses `ghuser_cpython` configuration key.
* Moved task `build-cpython-ghuser-components` from `build` to `grasshopper`.
* Moved task `build-ghuser-components` from `build` to `grasshopper`.

### Removed

Expand Down
3 changes: 2 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ invoke >=0.14
ruff
sphinx_compas2_theme
twine
wheel
wheel
toml
194 changes: 194 additions & 0 deletions src/compas_invocations2/grasshopper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
"""
Adapted from: https://github.com/diffCheckOrg/diffCheck/blob/main/invokes/yakerize.py

Yakerize.py was originally developed as part of the DiffCheck plugin by
Andrea Settimi, Damien Gilliard, Eleni Skevaki, Marirena Kladeftira (IBOIS, CRCL, EPFL) in 2024.
It is distributed under the MIT License, provided this attribution is retained.
"""

import os
import shutil
import tempfile

import invoke
import requests
import toml

from compas_invocations2.console import chdir

YAK_URL = r"https://files.mcneel.com/yak/tools/latest/yak.exe"


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")
with open(target_path, "wb") as f:
f.write(response.content)
return target_path
chenkasirer marked this conversation as resolved.
Show resolved Hide resolved


def _set_version_in_manifest(manifest_path: str, version: str):
with open(manifest_path, "r") as f:
lines = f.readlines()

new_lines = []
for line in lines:
if "{{ version }}" in line:
new_lines.append(line.replace("{{ version }}", version))
else:
new_lines.append(line)

with open(manifest_path, "w") as f:
f.writelines(new_lines)


def _clear_directory(path_to_dir):
for f in os.listdir(path_to_dir):
file_path = os.path.join(path_to_dir, f)
try:
if os.path.isfile(file_path) or os.path.islink(file_path):
os.unlink(file_path)
elif os.path.isdir(file_path):
shutil.rmtree(file_path)
except Exception as e:
raise invoke.Exit(f"Failed to delete {file_path}: {e}")


def _get_version_from_toml(toml_file: str) -> str:
pyproject_data = toml.load(toml_file)
version = pyproject_data.get("tool", {}).get("bumpversion", {}).get("current_version", None)
if not version:
raise invoke.Exit("Failed to get version from pyproject.toml. Please provide a version number.")
return version


def _get_user_object_path(context):
if hasattr(context, "ghuser_cpython"):
print("checking ghuser_cpython")
return os.path.join(context.base_folder, context.ghuser_cpython.target_dir)
elif hasattr(context, "ghuser"):
print("checking ghuser")
return os.path.join(context.base_folder, context.ghuser.target_dir)
else:
return None


@invoke.task(
help={
"manifest_path": "Path to the manifest file.",
"logo_path": "Path to the logo file.",
"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.",
"version": "(Optional) The version number to set in the manifest file.",
"target_rhino": "(Optional) The target Rhino version for the package. Defaults to 'rh8'.",
}
)
def yakerize(
ctx,
manifest_path: str,
logo_path: str,
gh_components_dir: str = None,
readme_path: str = None,
license_path: str = None,
version: str = None,
target_rhino: str = "rh8",
Copy link
Member

Choose a reason for hiding this comment

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

should we use rh# or #.0?

Copy link
Member Author

Choose a reason for hiding this comment

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

good point.. package manager allows minor version as well

Copy link
Member Author

Choose a reason for hiding this comment

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

added support for minor version!

) -> bool:
"""Create a Grasshopper YAK package from the current project."""
# https://developer.rhino3d.com/guides/yak/the-anatomy-of-a-package/
if target_rhino.split("_")[0] not in ["rh6", "rh7", "rh8"]:
raise invoke.Exit(
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)."""
)
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.")

readme_path = readme_path or os.path.join(ctx.base_folder, "README.md")
if not os.path.exists(readme_path):
raise invoke.Exit(f"Readme file not found at {readme_path}. Please provide a valid path.")

license_path = license_path or os.path.join(ctx.base_folder, "LICENSE")
if not os.path.exists(license_path):
raise invoke.Exit(f"License file not found at {license_path}. Please provide a valid path.")

version = version or _get_version_from_toml(os.path.join(ctx.base_folder, "pyproject.toml"))
target_dir = os.path.join(ctx.base_folder, "dist", "yak_package")

#####################################################################
# Copy manifest, logo, misc folder (readme, license, etc)
#####################################################################
# if target dit exists, make sure it's empty
if os.path.exists(target_dir) and os.path.isdir(target_dir):
_clear_directory(target_dir)
else:
os.makedirs(target_dir, exist_ok=False)

manifest_target = shutil.copy(manifest_path, target_dir)
_set_version_in_manifest(manifest_target, version)
shutil.copy(logo_path, target_dir)

path_miscdir: str = os.path.join(target_dir, "misc")
os.makedirs(path_miscdir, exist_ok=False)
shutil.copy(readme_path, path_miscdir)
shutil.copy(license_path, path_miscdir)

for f in os.listdir(gh_components_dir):
if f.endswith(".ghuser"):
shutil.copy(os.path.join(gh_components_dir, f), target_dir)

#####################################################################
# Yak exe
#####################################################################

# 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)
tomvanmele marked this conversation as resolved.
Show resolved Hide resolved
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")
Comment on lines +159 to +160
Copy link
Member

Choose a reason for hiding this comment

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

does chdir have any effect if we don't use ctx.run()?

Copy link
Member Author

Choose a reason for hiding this comment

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

mmh it appears so yea. yak packs the current directory. but wouldn't it though? chdir also just uses os

except Exception 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.")

# filename is what tells YAK the target Rhino version..?
taget_file = next((f for f in os.listdir(target_dir) if f.endswith(".yak")))
new_filename = taget_file.replace("any-any", f"{target_rhino}-any")
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."}
)
def publish_yak(ctx, yak_file: str, test_server: bool = False):
"""Publish a YAK package to the YAK server."""

if not os.path.exists(yak_file) or not os.path.isfile(yak_file):
raise invoke.Exit(f"Yak file not found at {yak_file}. Please provide a valid path.")
if not yak_file.endswith(".yak"):
raise invoke.Exit("Invalid file type. Must be a .yak file.")

with chdir(ctx.base_folder):
with tempfile.TemporaryDirectory("actions.publish_yak") as action_dir:
try:
_download_yak_executable(action_dir)
except ValueError:
raise invoke.Exit("Failed to download the yak executable")

yak_exe_path: str = os.path.join(action_dir, "yak.exe")
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}")
Loading