Skip to content

Yakerize #4

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

Merged
merged 17 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from 16 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
191 changes: 191 additions & 0 deletions src/compas_invocations2/grasshopper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
"""
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


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:
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:
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."""
if target_rhino not in ["rh6", "rh7", "rh8"]:
invoke.Exit("Invalid target Rhino version. Must be one of: rh6, rh7, rh8")

gh_components_dir = gh_components_dir or _get_user_object_path(ctx)
if not gh_components_dir:
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):
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):
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)
except ValueError:
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:
invoke.Exit(f"Failed to build the yak package: {e}")
if not any([f.endswith(".yak") for f in os.listdir(target_dir)]):
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):
invoke.Exit(f"Yak file not found at {yak_file}. Please provide a valid path.")
if not yak_file.endswith(".yak"):
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:
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