Compare commits
3 Commits
v1.0.0-rc2
...
v1.0.0-rc3
Author | SHA1 | Date | |
---|---|---|---|
|
b508c0ff2d | ||
|
44be6bb595 | ||
|
6eeda71fb7 |
35
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: "Generate test artifacts"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
cache: "pip"
|
||||
- name: Install dependencies
|
||||
run: pip install -r requirements.txt
|
||||
- name: Install colloid specific dependencies
|
||||
run: sudo apt update && sudo apt install -y sassc inkscape optipng
|
||||
- name: Generate themes
|
||||
run: |
|
||||
python patches/xfwm4/generate_assets.py
|
||||
|
||||
python ./build.py mocha --all-accents --zip -d $PWD/releases &&
|
||||
python ./build.py macchiato --all-accents --zip -d $PWD/releases &&
|
||||
python ./build.py frappe --all-accents --zip -d $PWD/releases &&
|
||||
python ./build.py latte --all-accents --zip -d $PWD/releases
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: '${{ github.sha }}-artifacts'
|
||||
path: ./releases/*.zip
|
||||
|
2
.github/workflows/release.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: "Release"
|
||||
name: "Mainline release"
|
||||
|
||||
on:
|
||||
push:
|
||||
|
@@ -17,4 +17,24 @@ Once the build script patches the submodule, it will write a file into
|
||||
The palette patches are generated through `whiskers`,
|
||||
so if you're changing them, they will need regenerated. Simply run `whiskers palette.tera` to rebuild them.
|
||||
|
||||
The process for building the theme is [documented in the README](./README.md#building).
|
||||
The process for building the theme is [documented in the README](./README.md#building).
|
||||
|
||||
### Running test builds
|
||||
We support building and publishing test builds from PRs. When you open PRs, the CI will automatically build with your changes and push an artifact
|
||||
which bundles all of the produced themes.
|
||||
|
||||
You can then download the artifacts as a zip (result should look similar to 7bff2448a81e36bf3b0e03bfbd649bebe6973ec7-artifacts.zip) and
|
||||
pass the path into `install.py` under the `--from-artifact` option:
|
||||
```bash
|
||||
python3 install.py mocha blue --dest ./build --from-artifact ~/downloads/7bff2448a81e36bf3b0e03bfbd649bebe6973ec7-artifacts.zip
|
||||
```
|
||||
|
||||
This will take the target flavor / accent out of the zip, and install it using the regular install process.
|
||||
|
||||
It is advised to pass a `--dest` when running in this mode, because the released zips follow the exact same naming scheme as regular builds.
|
||||
This wil cause conflicts when you install, if you already had that theme installed. Passing a different destination allows you to move the
|
||||
extracted folders to `~/.local/share/themes` yourself, adding a suffix as appropriate to avoid conflicts.
|
||||
|
||||
> [!WARNING]
|
||||
> If you pass `--link` to the install script when working from a PR, it will forcibly overwrite your `~/.config/gtk-4.0/` symlinks.
|
||||
> You will have to reinstall / relink to revert this.
|
||||
|
14
build.py
@@ -393,17 +393,16 @@ def make_assets(ctx: BuildContext):
|
||||
f"{output_dir}/metacity-1/thumbnail.png",
|
||||
)
|
||||
|
||||
# TODO: Make our own assets for this and patch them in with the patch system, then code it to be
|
||||
# {src_dir}/assets/xfwm4/assets{light_suffix}-Catppuccin/
|
||||
# where assets-Light-Catppuccin will have latte
|
||||
# nad assets-Catppuccin will have mocha or something
|
||||
for file in glob.glob(f"{SRC_DIR}/assets/xfwm4/assets{ctx.apply_suffix(IS_LIGHT)}/*.png"):
|
||||
xfwm4_assets = f"{THIS_DIR}/patches/xfwm4/generated/assets-catppuccin-{ctx.flavor.identifier}"
|
||||
for file in glob.glob(xfwm4_assets + '/*'):
|
||||
shutil.copy(file, f"{output_dir}/xfwm4")
|
||||
|
||||
for file in glob.glob(f"{SRC_DIR}/assets/xfwm4/assets{ctx.apply_suffix(IS_LIGHT)}-hdpi/*.png"):
|
||||
xfwm4_assets = xfwm4_assets + "-hdpi/*"
|
||||
for file in glob.glob(xfwm4_assets):
|
||||
shutil.copy(file, f"{output_dir}-hdpi/xfwm4")
|
||||
|
||||
for file in glob.glob(f"{SRC_DIR}/assets/xfwm4/assets{ctx.apply_suffix(IS_LIGHT)}-xhdpi/*.png"):
|
||||
xfwm4_assets = xfwm4_assets + "-xhdpi/*"
|
||||
for file in glob.glob(xfwm4_assets):
|
||||
shutil.copy(file, f"{output_dir}-xhdpi/xfwm4")
|
||||
|
||||
|
||||
@@ -491,7 +490,6 @@ def apply_colloid_patches():
|
||||
|
||||
logger.info("Patching finished.")
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
|
116
install.py
@@ -1,9 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os, zipfile, argparse, logging, io
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
from urllib.request import urlopen, Request
|
||||
from urllib.parse import urlparse
|
||||
|
||||
logger = logging.getLogger("catppuccin-gtk")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
@@ -20,6 +22,17 @@ class InstallContext:
|
||||
dest: Path
|
||||
link: bool
|
||||
|
||||
def build_info(self, include_url=True) -> str:
|
||||
url = build_release_url(self)
|
||||
info = f"""Installation info:
|
||||
flavor: {self.flavor}
|
||||
accent: {self.accent}
|
||||
dest: {self.dest.absolute()}
|
||||
link: {self.link}"""
|
||||
if include_url:
|
||||
info += f"\nremote_url: {url}"
|
||||
return info
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
@@ -49,11 +62,17 @@ def parse_args():
|
||||
"sapphire",
|
||||
"blue",
|
||||
"lavender",
|
||||
"all",
|
||||
],
|
||||
help="Accent of the theme.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--from-artifact",
|
||||
type=Path,
|
||||
dest="from_artifact",
|
||||
help="Install from an artifact instead of a mainline release, pass the artifact path (e.g 7bff2448a81e36bf3b0e03bfbd649bebe6973ec7-artifacts.zip)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--dest",
|
||||
"-d",
|
||||
@@ -81,21 +100,12 @@ def build_release_url(ctx: InstallContext) -> str:
|
||||
return f"{repo_root}/{release}/{zip_name}"
|
||||
|
||||
|
||||
def install(ctx: InstallContext):
|
||||
url = build_release_url(ctx)
|
||||
build_info = f"""Installation info:
|
||||
flavor: {ctx.flavor}
|
||||
accent: {ctx.accent}
|
||||
dest: {ctx.dest.absolute()}
|
||||
link: {ctx.link}
|
||||
|
||||
remote_url: {url}"""
|
||||
logger.info(build_info)
|
||||
httprequest = Request(url)
|
||||
def fetch_zip(url: str) -> Optional[zipfile.ZipFile]:
|
||||
req = Request(url)
|
||||
|
||||
zip_file = None
|
||||
logger.info("Starting download...")
|
||||
with urlopen(httprequest) as response:
|
||||
with urlopen(req) as response:
|
||||
logger.info(f"Response status: {response.status}")
|
||||
zip_file = zipfile.ZipFile(io.BytesIO(response.read()))
|
||||
logger.info("Download finished, zip is valid")
|
||||
@@ -104,28 +114,81 @@ def install(ctx: InstallContext):
|
||||
first_bad_file = zip_file.testzip()
|
||||
if first_bad_file is not None:
|
||||
logger.error(f'Zip appears to be corrupt, first bad file is "{first_bad_file}"')
|
||||
return
|
||||
return None
|
||||
logger.info("Download verified")
|
||||
return zip_file
|
||||
|
||||
|
||||
def add_libadwaita_links(ctx: InstallContext, rewrite: True):
|
||||
dir_name = (
|
||||
ctx.dest / f"catppuccin-{ctx.flavor}-{ctx.accent}-standard+default" / "gtk-4.0"
|
||||
).absolute()
|
||||
gtk4_dir = (Path(os.path.expanduser("~")) / ".config" / "gtk-4.0").absolute()
|
||||
os.makedirs(gtk4_dir, exist_ok=True)
|
||||
|
||||
logger.info("Adding symlinks for libadwaita")
|
||||
logger.info(f"Root: {dir_name}")
|
||||
logger.info(f"Target: {gtk4_dir}")
|
||||
try:
|
||||
if rewrite:
|
||||
os.remove(dir_name / "assets", gtk4_dir / "assets")
|
||||
os.remove(dir_name / "gtk.css", gtk4_dir / "gtk.css")
|
||||
os.remove(dir_name / "gtk-dark.css", gtk4_dir / "gtk-dark.css")
|
||||
except FileNotFoundError:
|
||||
logger.debug("Ignoring FileNotFound in symlink rewrite")
|
||||
|
||||
os.symlink(dir_name / "assets", gtk4_dir / "assets")
|
||||
os.symlink(dir_name / "gtk.css", gtk4_dir / "gtk.css")
|
||||
os.symlink(dir_name / "gtk-dark.css", gtk4_dir / "gtk-dark.css")
|
||||
|
||||
|
||||
def install(ctx: InstallContext):
|
||||
url = build_release_url(ctx)
|
||||
logger.info(ctx.build_info())
|
||||
|
||||
zip_file = fetch_zip(url)
|
||||
if zip_file is None:
|
||||
return
|
||||
|
||||
logger.info("Extracting...")
|
||||
zip_file.extractall(ctx.dest)
|
||||
logger.info("Extraction complete")
|
||||
|
||||
if ctx.link:
|
||||
dir_name = (ctx.dest / f"catppuccin-{ctx.flavor}-{ctx.accent}-standard+default" / 'gtk-4.0').absolute()
|
||||
gtk4_dir = (Path(os.path.expanduser('~')) / '.config' / 'gtk-4.0').absolute()
|
||||
os.makedirs(gtk4_dir, exist_ok=True)
|
||||
add_libadwaita_links(ctx)
|
||||
|
||||
logger.info("Adding symlinks for libadwaita")
|
||||
logger.info(f'Root: {dir_name}')
|
||||
logger.info(f'Target: {gtk4_dir}')
|
||||
os.symlink(dir_name / 'assets', gtk4_dir / 'assets')
|
||||
os.symlink(dir_name / 'gtk.css', gtk4_dir / 'gtk.css')
|
||||
os.symlink(dir_name / 'gtk-dark.css', gtk4_dir / 'gtk-dark.css')
|
||||
def install_from_artifact(ctx: InstallContext, artifact_path: Path):
|
||||
# Working from a pull request, special case it
|
||||
logger.info(f"Extracting artifact from '{artifact_path}'")
|
||||
artifacts = zipfile.ZipFile(artifact_path)
|
||||
|
||||
logger.info("Verifying artifact...")
|
||||
first_bad_file = artifacts.testzip()
|
||||
if first_bad_file is not None:
|
||||
logger.error(f'Zip appears to be corrupt, first bad file is "{first_bad_file}"')
|
||||
return None
|
||||
logger.info("Artifact verified")
|
||||
|
||||
logger.info(ctx.build_info(False))
|
||||
|
||||
# The zip, inside the artifacts, that we want to pull out
|
||||
zip_name = f"catppuccin-{ctx.flavor}-{ctx.accent}-standard+default.zip"
|
||||
logger.info(f"Pulling '{zip_name}' from the artifacts")
|
||||
info = artifacts.getinfo(zip_name)
|
||||
|
||||
logger.info("Extracting the artifact...")
|
||||
artifact = zipfile.ZipFile(io.BytesIO(artifacts.read(info)))
|
||||
artifact.extractall(ctx.dest)
|
||||
logger.info("Extraction complete")
|
||||
|
||||
if ctx.link:
|
||||
logger.info("Adding links (with rewrite)")
|
||||
add_libadwaita_links(ctx, True)
|
||||
logger.info("Links added")
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
dest = Path(os.path.expanduser("~")) / ".local" / "share" / "themes"
|
||||
os.makedirs(dest, exist_ok=True)
|
||||
|
||||
@@ -136,9 +199,12 @@ def main():
|
||||
flavor=args.flavor, accent=args.accent, dest=dest, link=args.link
|
||||
)
|
||||
|
||||
install(ctx)
|
||||
if args.from_artifact:
|
||||
install_from_artifact(ctx, args.from_artifact)
|
||||
return
|
||||
|
||||
logger.info('Theme installation complete!')
|
||||
install(ctx)
|
||||
logger.info("Theme installation complete!")
|
||||
|
||||
|
||||
try:
|
||||
|
2
patches/xfwm4/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
generated/
|
||||
patched/
|
Before Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 46 KiB |
244
patches/xfwm4/generate_assets.py
Normal file
@@ -0,0 +1,244 @@
|
||||
from typing import List
|
||||
from catppuccin import PALETTE
|
||||
from catppuccin.models import Flavor
|
||||
|
||||
import re, os, shutil, subprocess, time
|
||||
from dataclasses import dataclass
|
||||
|
||||
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
INDEX = [
|
||||
"close-active",
|
||||
"close-inactive",
|
||||
"close-prelight",
|
||||
"close-pressed",
|
||||
"hide-active",
|
||||
"hide-inactive",
|
||||
"hide-prelight",
|
||||
"hide-pressed",
|
||||
"maximize-active",
|
||||
"maximize-inactive",
|
||||
"maximize-prelight",
|
||||
"maximize-pressed",
|
||||
"maximize-toggled-active",
|
||||
"maximize-toggled-inactive",
|
||||
"maximize-toggled-prelight",
|
||||
"maximize-toggled-pressed",
|
||||
"menu-active",
|
||||
"menu-inactive",
|
||||
"menu-prelight",
|
||||
"menu-pressed",
|
||||
"shade-active",
|
||||
"shade-inactive",
|
||||
"shade-prelight",
|
||||
"shade-pressed",
|
||||
"shade-toggled-active",
|
||||
"shade-toggled-inactive",
|
||||
"shade-toggled-prelight",
|
||||
"shade-toggled-pressed",
|
||||
"stick-active",
|
||||
"stick-inactive",
|
||||
"stick-prelight",
|
||||
"stick-pressed",
|
||||
"stick-toggled-active",
|
||||
"stick-toggled-inactive",
|
||||
"stick-toggled-prelight",
|
||||
"stick-toggled-pressed",
|
||||
"title-1-active",
|
||||
"title-1-inactive",
|
||||
"title-2-active",
|
||||
"title-2-inactive",
|
||||
"title-3-active",
|
||||
"title-3-inactive",
|
||||
"title-4-active",
|
||||
"title-4-inactive",
|
||||
"title-5-active",
|
||||
"title-5-inactive",
|
||||
"top-left-active",
|
||||
"top-left-inactive",
|
||||
"top-right-active",
|
||||
"top-right-inactive",
|
||||
"left-active",
|
||||
"left-inactive",
|
||||
"right-active",
|
||||
"right-inactive",
|
||||
"bottom-active",
|
||||
"bottom-inactive",
|
||||
"bottom-left-active",
|
||||
"bottom-left-inactive",
|
||||
"bottom-right-active",
|
||||
"bottom-right-inactive",
|
||||
]
|
||||
|
||||
|
||||
def subst_text(path, _from, to):
|
||||
with open(path, "r+") as f:
|
||||
content = f.read()
|
||||
f.seek(0)
|
||||
f.truncate()
|
||||
f.write(re.sub(_from, to, content))
|
||||
|
||||
|
||||
def generate_for_flavor(flavor: Flavor):
|
||||
# Setup the palette
|
||||
palette = flavor.colors
|
||||
|
||||
close_color=f'#{palette.red.hex}'
|
||||
max_color=f'#{palette.green.hex}'
|
||||
min_color=f'#{palette.yellow.hex}'
|
||||
|
||||
# We expand the source assets into the 4 flavors, but need to map between
|
||||
# Their definition of dark and ours. This means that latte will get the -light assets
|
||||
# and the rest get the -dark assets to work from
|
||||
color = "-dark"
|
||||
if not flavor.dark:
|
||||
color = "-light"
|
||||
|
||||
# Make a directory for our patched SVGs to live in before compilation
|
||||
odir = f"{THIS_DIR}/patched"
|
||||
os.makedirs(odir, exist_ok=True)
|
||||
|
||||
# Copy the base assets into the output
|
||||
shutil.copy(
|
||||
f"{THIS_DIR}/assets{color}.svg",
|
||||
f"{odir}/assets-catppuccin-{flavor.identifier}.svg",
|
||||
)
|
||||
shutil.copy(
|
||||
f"{THIS_DIR}/assets{color}-normal.svg",
|
||||
f"{odir}/assets-catppuccin-{flavor.identifier}-normal.svg",
|
||||
)
|
||||
|
||||
# Patch all the SVGs
|
||||
path = f"{odir}/assets-catppuccin-{flavor.identifier}-normal.svg"
|
||||
subst_text(path, "#fd5f51", close_color)
|
||||
subst_text(path, "#38c76a", max_color)
|
||||
subst_text(path, "#fdbe04", min_color)
|
||||
|
||||
headerbar = palette.base.hex
|
||||
headerbar_backdrop = palette.mantle.hex
|
||||
|
||||
if flavor.dark:
|
||||
path = f"{odir}/assets-catppuccin-{flavor.identifier}.svg"
|
||||
subst_text(path, "#242424", headerbar)
|
||||
subst_text(path, "#2c2c2c", headerbar_backdrop)
|
||||
|
||||
path = f"{odir}/assets-catppuccin-{flavor.identifier}-normal.svg"
|
||||
subst_text(path, "#242424", headerbar)
|
||||
subst_text(path, "#2c2c2c", headerbar_backdrop)
|
||||
else:
|
||||
path = f"{odir}/assets-catppuccin-{flavor.identifier}.svg"
|
||||
subst_text(path, "#f2f2f2", headerbar)
|
||||
subst_text(path, "#fafafa", headerbar_backdrop)
|
||||
|
||||
path = f"{odir}/assets-catppuccin-{flavor.identifier}-normal.svg"
|
||||
subst_text(path, "#f2f2f2", headerbar)
|
||||
subst_text(path, "#fafafa", headerbar_backdrop)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkerInput:
|
||||
output_path: str
|
||||
output_dir: str
|
||||
input_path: str
|
||||
dpi: str
|
||||
ident: str
|
||||
|
||||
|
||||
def call_subprocesses(inp: WorkerInput):
|
||||
inkscape = "inkscape"
|
||||
optipng = "optipng"
|
||||
|
||||
return [
|
||||
subprocess.Popen(
|
||||
[
|
||||
inkscape,
|
||||
"--export-id",
|
||||
inp.ident,
|
||||
"--export-id-only",
|
||||
"--export-dpi",
|
||||
inp.dpi,
|
||||
"--export-filename",
|
||||
inp.output_path,
|
||||
inp.input_path,
|
||||
],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.STDOUT,
|
||||
),
|
||||
subprocess.Popen(
|
||||
[optipng, "-o7", "--quiet", inp.output_path],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.STDOUT,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class RenderState:
|
||||
tasks: List[subprocess.Popen]
|
||||
input_root: str
|
||||
output_root: str
|
||||
|
||||
|
||||
screen_to_dpi = {
|
||||
"-hdpi": "144",
|
||||
"-xhdpi": "192",
|
||||
}
|
||||
|
||||
def render_for_screen(state: RenderState, flavor: Flavor, screen: str, ident: str):
|
||||
# NOTE: We do not generate for the -normal variant currently, that would just be
|
||||
# a stupid amount of compute and time for little benefit
|
||||
src_file = f"{state.input_root}/assets-catppuccin-{flavor.identifier}.svg"
|
||||
|
||||
output_dir = f"{state.output_root}/assets-catppuccin-{flavor.identifier}{screen}"
|
||||
output_path = f"{output_dir}/{ident}.png"
|
||||
|
||||
dpi = screen_to_dpi.get(screen, "96")
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
if os.path.exists(output_path):
|
||||
print(f"Skipping '{output_path}', already generated")
|
||||
else:
|
||||
new_tasks = call_subprocesses(
|
||||
WorkerInput(
|
||||
output_path=output_path,
|
||||
output_dir=output_dir,
|
||||
input_path=src_file,
|
||||
dpi=dpi,
|
||||
ident=ident,
|
||||
)
|
||||
)
|
||||
state.tasks.extend(new_tasks)
|
||||
|
||||
|
||||
def render_for_flavor(flavor: Flavor, state: RenderState):
|
||||
print(
|
||||
f"Starting render tasks for {flavor.identifier}"
|
||||
)
|
||||
for ident in INDEX:
|
||||
render_for_screen(state=state, flavor=flavor, screen="", ident=ident)
|
||||
render_for_screen(state=state, flavor=flavor, screen="-hdpi", ident=ident)
|
||||
render_for_screen(state=state, flavor=flavor, screen="-xhdpi", ident=ident)
|
||||
|
||||
|
||||
generate_for_flavor(PALETTE.mocha)
|
||||
generate_for_flavor(PALETTE.latte)
|
||||
generate_for_flavor(PALETTE.macchiato)
|
||||
generate_for_flavor(PALETTE.frappe)
|
||||
|
||||
state = RenderState(
|
||||
tasks=[], input_root=f"{THIS_DIR}/patched", output_root=f"{THIS_DIR}/generated"
|
||||
)
|
||||
start_time = time.time()
|
||||
|
||||
render_for_flavor(PALETTE.mocha, state)
|
||||
render_for_flavor(PALETTE.latte, state)
|
||||
render_for_flavor(PALETTE.macchiato, state)
|
||||
render_for_flavor(PALETTE.frappe, state)
|
||||
|
||||
for task in state.tasks:
|
||||
task.wait()
|
||||
|
||||
end_time = time.time() - start_time
|
||||
print(f"Generation complete in {end_time} seconds")
|
||||
shutil.rmtree(state.input_root)
|