Skip to content

Commit

Permalink
Merge branch 'main' into fix/wrong-escape-sequences
Browse files Browse the repository at this point in the history
  • Loading branch information
aragon999 authored Feb 1, 2025
2 parents 4715633 + d4e223a commit 8a094a3
Show file tree
Hide file tree
Showing 46 changed files with 778 additions and 1,135 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ jobs:
name: python-package-distributions
path: dist/
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@v1.9.0
uses: pypa/gh-action-pypi-publish@v1.12.2
with:
# repository-url: https://test.pypi.org/legacy/ # for testing purposes
verify-metadata: false # twine previously didn't verify metadata when uploading
5 changes: 4 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.9", "3.10", "3.11", "3.13"]
platform: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v4
Expand All @@ -47,6 +47,9 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install Cairo (Ubuntu)
if: matrix.platform == 'ubuntu-latest'
run: sudo apt-get install libcairo2-dev
- name: Install packages
run: |
pip install '.[qa]'
Expand Down
10 changes: 10 additions & 0 deletions Lib/gftools/argparse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from argparse import ArgumentParser


class GFArgumentParser(ArgumentParser):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.add_argument(
"--show-tracebacks", action="store_true", help="Show tracebacks"
)
65 changes: 52 additions & 13 deletions Lib/gftools/builder/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@
from tempfile import NamedTemporaryFile, gettempdir
from typing import Any, Dict, List, Union

from gftools.builder.fontc import FontcArgs
import networkx as nx
import strictyaml
import yaml
from fontmake.font_project import FontProject
from ninja import _program
from ninja.ninja_syntax import Writer, escape_path
from typing import Union

from gftools.builder.file import File
from gftools.builder.operations import OperationBase, known_operations
from gftools.builder.operations import OperationBase, OperationRegistry
from gftools.builder.operations.copy import Copy
from gftools.builder.recipeproviders import get_provider
from gftools.builder.schema import BASE_SCHEMA
Expand All @@ -36,7 +38,11 @@ class GFBuilder:
config: dict
recipe: Recipe

def __init__(self, config: Union[dict, str]):
def __init__(
self,
config: Union[dict, str],
fontc_args=FontcArgs(None),
):
if isinstance(config, str):
parentpath = Path(config).resolve().parent
with open(config, "r") as file:
Expand All @@ -54,8 +60,11 @@ def __init__(self, config: Union[dict, str]):
else:
self._orig_config = yaml.dump(config)
self.config = config
fontc_args.modify_config(self.config)

self.writer = Writer(open("build.ninja", "w"))
self.known_operations = OperationRegistry(use_fontc=fontc_args.use_fontc)
self.ninja_file_name = fontc_args.build_file_name()
self.writer = Writer(open(self.ninja_file_name, "w"))
self.named_files = {}
self.used_operations = set([])
self.graph = nx.DiGraph()
Expand Down Expand Up @@ -156,9 +165,9 @@ def glyphs_to_ufo(self, source):

def operation_step_to_object(self, step):
operation = step.get("operation") or step.get("postprocess")
if operation not in known_operations:
cls = self.known_operations.get(operation)
if cls is None:
raise ValueError(f"Unknown operation {operation}")
cls = known_operations[operation]
if operation not in self.used_operations:
self.used_operations.add(operation)
cls.write_rules(self.writer)
Expand Down Expand Up @@ -328,7 +337,9 @@ def walk_graph(self):
def draw_graph(self):
import pydot

dot = subprocess.run(["ninja", "-t", "graph"], capture_output=True)
dot = subprocess.run(
["ninja", "-t", "graph", "-f", self.ninja_file_name], capture_output=True
)
graphs = pydot.graph_from_dot_data(dot.stdout.decode("utf-8"))
targets = self.recipe.keys()
if graphs and graphs[0]:
Expand All @@ -349,12 +360,21 @@ def draw_graph(self):
print("Could not parse ninja build file")

def clean(self):
for file in ["./build.ninja", "./.ninja_log"]:
if os.path.exists(file):
os.remove(file)
if hasattr(self, "config") and isinstance(self.config, dict):
cleanUp = self.config.get("cleanUp")
if cleanUp == True:
print("Cleaning up temporary files...")

for file in [self.ninja_file_name, "./.ninja_log"]:
if os.path.exists(file):
os.remove(file)

if os.path.exists("instance_ufos"):
shutil.rmtree("instance_ufos")
if os.path.exists("instance_ufos"):
shutil.rmtree("instance_ufos")

print("Done cleaning up temporary files")
else:
print("Configuration not found or invalid, skipping cleanup.")


def main(args=None):
Expand All @@ -372,8 +392,27 @@ def main(args=None):
help="Just generate and output recipe from recipe builder",
action="store_true",
)
parser.add_argument(
"--experimental-fontc",
help=f"Use fontc instead of fontmake. Argument is path to the fontc executable",
type=Path,
)

parser.add_argument(
"--experimental-simple-output",
help="generate a reduced set of targets, and copy them to the provided directory",
type=Path,
)

parser.add_argument(
"--experimental-single-source",
help="only compile the single named source file",
type=str,
)

parser.add_argument("config", help="Path to config file or source file", nargs="+")
args = parser.parse_args(args)
fontc_args = FontcArgs(args)
yaml_files = []
source_files = []
for config in args.config:
Expand All @@ -395,7 +434,7 @@ def main(args=None):
raise ValueError("Only one config file can be given for now")
config = args.config[0]

pd = GFBuilder(config)
pd = GFBuilder(config, fontc_args=fontc_args)
if args.generate:
config = pd.config
config["recipe"] = pd.recipe
Expand All @@ -408,4 +447,4 @@ def main(args=None):
pd.draw_graph()
if not args.no_ninja:
atexit.register(pd.clean)
raise SystemExit(_program("ninja", []))
raise SystemExit(_program("ninja", ["-f", pd.ninja_file_name]))
6 changes: 6 additions & 0 deletions Lib/gftools/builder/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ def is_designspace(self):
def is_font_source(self):
return self.is_glyphs or self.is_ufo or self.is_designspace

@cached_property
def is_variable(self) -> bool:
return (self.is_glyphs and len(self.gsfont.masters) > 1) or (
self.is_designspace and len(self.designspace.sources) > 1
)

@cached_property
def gsfont(self):
if self.is_glyphs:
Expand Down
98 changes: 98 additions & 0 deletions Lib/gftools/builder/fontc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""functionality for running fontc via gftools
gftools has a few special flags that allow it to use fontc, an alternative
font compiler (https://github.com/googlefonts/fontc).
This module exists to keep the logic related to fontc in one place, and not
dirty up everything else.
"""

from argparse import Namespace
from pathlib import Path
from typing import Union
import time

from gftools.builder.file import File
from gftools.builder.operations.fontc import set_global_fontc_path


class FontcArgs:
# init with 'None' returns a default obj where everything is None
def __init__(self, args: Union[Namespace, None]) -> None:
if not args:
self.simple_output_path = None
self.fontc_bin_path = None
self.single_source = None
return
self.simple_output_path = abspath(args.experimental_simple_output)
self.fontc_bin_path = abspath(args.experimental_fontc)
self.single_source = args.experimental_single_source
if self.fontc_bin_path:
if not self.fontc_bin_path.is_file():
raise ValueError(f"fontc does not exist at {self.fontc_bin_path}")
set_global_fontc_path(self.fontc_bin_path)

@property
def use_fontc(self) -> bool:
return self.fontc_bin_path is not None

def build_file_name(self) -> str:
if self.fontc_bin_path or self.simple_output_path:
# if we're running for fontc we want uniquely named build files,
# to ensure they don't collide
return f"build-{time.time_ns()}.ninja"
else:
# otherwise just ues the default name
return "build.ninja"

# update the config dictionary based on our special needs
def modify_config(self, config: dict):
if self.single_source:
filtered_sources = [s for s in config["sources"] if self.single_source in s]
n_sources = len(filtered_sources)
if n_sources != 1:
raise ValueError(
f"--exerimental-single-source {self.single_source} must match exactly one of {config['sources']} (matched {n_sources}) "
)
config["sources"] = filtered_sources

if self.fontc_bin_path or self.simple_output_path:
# we stash this flag here to pass it down to the recipe provider
config["use_fontc"] = self.fontc_bin_path
config["buildWebfont"] = False
config["buildSmallCap"] = False
config["splitItalic"] = False
config["cleanUp"] = True
# disable running ttfautohint, because we had a segfault
config["autohintTTF"] = False
# set --no-production-names, because it's easier to debug
extra_args = config.get("extraFontmakeArgs") or ""
extra_args += " --no-production-names --drop-implied-oncurves"
config["extraFontmakeArgs"] = extra_args
# override config to turn not build instances if we're variable
if self.will_build_variable_font(config):
config["buildStatic"] = False
# if the font doesn't explicitly request CFF, just build TT outlines
# if the font _only_ wants CFF outlines, we will try to build them
# ( but fail on fontc for now) (but is this even a thing?)
elif config.get("buildTTF", True):
config["buildOTF"] = False
if self.simple_output_path:
output_dir = str(self.simple_output_path)
# we dump everything into one dir in this case
config["outputDir"] = str(output_dir)
config["ttDir"] = str(output_dir)
config["otDir"] = str(output_dir)
config["vfDir"] = str(output_dir)

def will_build_variable_font(self, config: dict) -> bool:
# if config explicitly says dont build variable, believe it
if not config.get("buildVariable", True):
return False

source = File(config["sources"][0])
return source.is_variable


def abspath(path: Union[Path, None]) -> Union[Path, None]:
return path.resolve() if path else None
58 changes: 45 additions & 13 deletions Lib/gftools/builder/operations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import sys
from os.path import dirname
from tempfile import NamedTemporaryFile
from typing import Dict

from gftools.builder.file import File
from gftools.utils import shell_quote
Expand Down Expand Up @@ -150,17 +151,48 @@ def variables(self):
return vars


known_operations = {}
class OperationRegistry:
def __init__(self, use_fontc: bool):
self.known_operations = get_known_operations()
self.use_fontc = use_fontc

for mod in pkgutil.iter_modules([dirname(__file__)]):
imp = importlib.import_module("gftools.builder.operations." + mod.name)
classes = [
(name, cls)
for name, cls in inspect.getmembers(sys.modules[imp.__name__], inspect.isclass)
if "OperationBase" not in name and issubclass(cls, OperationBase)
]
if len(classes) > 1:
raise ValueError(
f"Too many classes in module gftools.builder.operations.{mod.name}"
)
known_operations[mod.name] = classes[0][1]
def get(self, operation_name: str):
if self.use_fontc:
if operation_name == "buildVariable":
# if we import this at the top level it's a circular import error
from .fontc.fontcBuildVariable import FontcBuildVariable

return FontcBuildVariable
if operation_name == "buildTTF":
from .fontc.fontcBuildTTF import FontcBuildTTF

return FontcBuildTTF

if operation_name == "buildOTF":
from .fontc.fontcBuildOTF import FontcBuildOTF

return FontcBuildOTF

return self.known_operations.get(operation_name)


def get_known_operations() -> Dict[str, OperationBase]:
known_operations = {}

for mod in pkgutil.iter_modules([dirname(__file__)]):
if "fontc" in mod.name:
continue
imp = importlib.import_module("gftools.builder.operations." + mod.name)
classes = [
(name, cls)
for name, cls in inspect.getmembers(
sys.modules[imp.__name__], inspect.isclass
)
if "OperationBase" not in name and issubclass(cls, OperationBase)
]
if len(classes) > 1:
raise ValueError(
f"Too many classes in module gftools.builder.operations.{mod.name}"
)
known_operations[mod.name] = classes[0][1]
return known_operations
2 changes: 1 addition & 1 deletion Lib/gftools/builder/operations/autohintOTF.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@

class AutohintOTF(OperationBase):
description = "Run otfautohint"
rule = 'otfautohint $args -o "$out" "$in" \\|\\| otfautohint $args -o "$out" "$in" --no-zones-stems'
rule = r'otfautohint $args -o "$out" "$in" \|\| otfautohint $args -o "$out" "$in" --no-zones-stems'
Loading

0 comments on commit 8a094a3

Please sign in to comment.