# Copyright 2026 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 EAPI=8 RUST_MIN_VER="1.95.0" CRATES="" declare -A GIT_CRATES=( [craft-print]='https://github.com/storytold/craft-libs;d991bf6792f8ade99c51358bf3c1d902c8cd96b5;craft-libs-%commit%/crates/print' [craft-ui]='https://github.com/storytold/craft-libs;5c874d51eeb3de70e735c8ce87a59a7dedd47675;craft-libs-%commit%/crates/ui' [hayro-interpret]='https://github.com/storytold/pdfcraft;b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9;pdfcraft-%commit%/vendor/hayro-interpret' [hayro-jbig2]='https://github.com/storytold/pdfcraft;b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9;pdfcraft-%commit%/vendor/hayro-jbig2' [hayro-syntax]='https://github.com/storytold/pdfcraft;b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9;pdfcraft-%commit%/vendor/hayro-syntax' [hayro]='https://github.com/storytold/pdfcraft;b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9;pdfcraft-%commit%/vendor/hayro' [winit]='https://github.com/skylartaylor/winit;3d51077fd1a966f84df9ef287839dc84fd5b4ca7;winit-%commit%' ) inherit cargo xdg DESCRIPTION="Open-source native page layout" HOMEPAGE="https://getartcraft.com/apps/designcraft https://github.com/storytold/designcraft" SRC_URI="https://github.com/storytold/designcraft/archive/refs/tags/v${PV}.tar.gz -> ${P}.tar.gz" SRC_URI+=" https://github.com/0x6d6e647a/mndz-overlay-assets/releases/download/designcraft-${PV}/designcraft-${PV}-crates.tar.xz" SRC_URI+=" ${CARGO_CRATE_URIS}" LICENSE="|| ( MIT Apache-2.0 )" # Dependent crate licenses LICENSE+=" Apache-2.0 BSD-2 BSD Boost-1.0 ISC MIT MPL-2.0 OFL-1.1 UbuntuFontLicense-1.0 Unicode-3.0 ZLIB " # assets/fonts are OFL-1.1. assets/hyphenation embeds the public-domain # Moby Hyphenator II list and patterns trained from it. LICENSE+="OFL-1.1 public-domain CC0-1.0" SLOT="0" KEYWORDS="-* ~amd64 ~arm ~arm64 ~loong ~ppc64 ~riscv ~s390 ~sparc ~x86" IUSE="+gui +cli +portal +X +wayland test" REQUIRED_USE="|| ( gui cli ) gui? ( || ( X wayland ) )" RESTRICT="!test? ( test )" # gui still needs xdg-open. RDEPEND=" gui? ( sys-apps/dbus x11-misc/xdg-utils media-libs/libglvnd media-libs/vulkan-loader x11-libs/libxkbcommon portal? ( sys-apps/xdg-desktop-portal ) !portal? ( gnome-base/zenity ) ) X? ( x11-libs/libX11 x11-libs/libXcursor x11-libs/libXi x11-libs/libXrandr x11-libs/libxkbcommon[X] ) wayland? ( dev-libs/wayland ) " BDEPEND="media-fonts/craft-fonts:=" src_prepare() { default # cargo.eclass writes [patch] into CARGO_HOME/config.toml. On Cargo 1.97 # that table does not satisfy a git dependency listed under [patch.*]. # Rewrite each one-line pin from GIT_CRATES onto its unpacked checkout. local -a pin_args=() local name spec commit dir for name in "${!GIT_CRATES[@]}"; do spec="${GIT_CRATES[${name}]}" IFS=';' read -r _ commit dir _ <<< "${spec}" [[ -n ${commit} && -n ${dir} ]] || die "GIT_CRATES ${name} has no commit and directory" dir=${dir//%commit%/${commit}} pin_args+=("${name}" "${commit}" "${WORKDIR}/${dir}") done python3 - "${S}/Cargo.toml" "${pin_args[@]}" <<'PY' || die "rewrite [patch.*] git pins" import re import sys from pathlib import Path toml_path = Path(sys.argv[1]) if not toml_path.is_file(): sys.exit(f"unpacked Cargo.toml missing: {toml_path}") pin_args = sys.argv[2:] if len(pin_args) % 3 != 0: sys.exit("git pin arguments are not name, commit, path triples") pins = {} for index in range(0, len(pin_args), 3): pin_name, pin_commit, pin_path = pin_args[index : index + 3] pins[(pin_name, pin_commit)] = pin_path try: text = toml_path.read_bytes().decode("utf-8") except UnicodeError: sys.exit(f"cannot parse patch git pin: {toml_path} is not utf-8") lines = text.splitlines(keepends=True) GIT_KEY = re.compile(r"(^|[{, \t])git[ \t]*=") ASSIGN_START = re.compile( r"^(?P[ \t]*)(?P[A-Za-z0-9_-]+)[ \t]*=[ \t]*(?P.*)$" ) KEY_VALUE = re.compile( r"^(?P[A-Za-z0-9_-]+)[ \t]*=[ \t]*(?P.*)$" ) GIT_SOURCE_KEYS = {"git", "rev", "branch", "tag"} def die(message): sys.exit(message) def code_portion(body): out = [] in_str = False escape = False quote = "" for ch in body: if in_str: out.append(ch) if escape: escape = False elif ch == "\\": escape = True elif ch == quote: in_str = False continue if ch in "\"'": in_str = True quote = ch out.append(ch) continue if ch == "#": break out.append(ch) return "".join(out).rstrip() def split_ending(line): if line.endswith("\r\n"): return line[:-2], "\r\n" if line.endswith("\n"): return line[:-1], "\n" if line.endswith("\r"): return line[:-1], "\r" return line, "" def brace_delta(fragment): depth = 0 in_str = False escape = False quote = "" for ch in fragment: if in_str: if escape: escape = False elif ch == "\\": escape = True elif ch == quote: in_str = False continue if ch in "\"'": in_str = True quote = ch continue if ch == "{": depth += 1 elif ch == "}": depth -= 1 return depth def split_inline(body): parts = [] buf = [] depth_brace = 0 depth_bracket = 0 in_str = False escape = False quote = "" for ch in body: if in_str: buf.append(ch) if escape: escape = False elif ch == "\\": escape = True elif ch == quote: in_str = False continue if ch in "\"'": in_str = True quote = ch buf.append(ch) continue if ch == "{": depth_brace += 1 elif ch == "}": if depth_brace == 0: return None depth_brace -= 1 elif ch == "[": depth_bracket += 1 elif ch == "]": if depth_bracket == 0: return None depth_bracket -= 1 if ch == "," and depth_brace == 0 and depth_bracket == 0: parts.append("".join(buf).strip()) buf = [] continue buf.append(ch) if in_str or depth_brace or depth_bracket: return None tail = "".join(buf).strip() if tail: parts.append(tail) return parts def unquote(value): if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'": return value[1:-1] return None def toml_escape(value): return value.replace("\\", "\\\\").replace('"', '\\"') def rewrite_body(name, body): parts = split_inline(body) if parts is None: die(f"cannot parse patch git pin {name}") items = [] rev = None saw_git = False for part in parts: if part == "": continue match = KEY_VALUE.match(part) if not match: die(f"cannot parse patch git pin {name}") key = match.group("key") value = match.group("value").strip() if key == "git": saw_git = True if key == "rev": rev = unquote(value) if rev is None: die(f"cannot parse patch git pin {name}") items.append((key, value, key in GIT_SOURCE_KEYS)) if not saw_git or not rev: die(f"cannot parse patch git pin {name}") path = pins.get((name, rev)) if path is None: die(f"GIT_CRATES has no {name} rev {rev}") checkout = Path(path) / "Cargo.toml" if not checkout.is_file(): die(f"unpacked Cargo.toml missing for {name}: {checkout}") rendered = [] placed = False escaped = toml_escape(path) for key, value, is_source in items: if is_source: if not placed: rendered.append(f'path = "{escaped}"') placed = True continue rendered.append(f"{key} = {value}") return "{ " + ", ".join(rendered) + " }" out = [] in_patch = False index = 0 while index < len(lines): line = lines[index] body, newline = split_ending(line) code = code_portion(body) stripped = code.lstrip() if stripped.startswith("["): in_patch = stripped.startswith("[patch.") out.append(line) index += 1 continue if not in_patch or stripped == "" or stripped.startswith("#"): out.append(line) index += 1 continue if GIT_KEY.search(code) is None and "{" not in code: out.append(line) index += 1 continue match = ASSIGN_START.match(code) if not match: die("cannot parse patch git pin") indent = match.group("indent") name = match.group("name") rest = match.group("rest").strip() depth = brace_delta(code) if depth < 0 or not rest.startswith("{"): die(f"cannot parse patch git pin {name}") block_lines = [line] block_codes = [code] cursor = index while depth > 0: cursor += 1 if cursor >= len(lines): die(f"cannot parse patch git pin {name}") next_line = lines[cursor] next_body, _next_newline = split_ending(next_line) next_code = code_portion(next_body) block_lines.append(next_line) block_codes.append(next_code) depth += brace_delta(next_code) if depth < 0: die(f"cannot parse patch git pin {name}") if cursor != index: if GIT_KEY.search("\n".join(block_codes)): die(f"cannot parse patch git pin {name}") out.extend(block_lines) index = cursor + 1 continue if GIT_KEY.search(code) is None: out.append(line) index += 1 continue if not rest.endswith("}"): die(f"cannot parse patch git pin {name}") rewritten = rewrite_body(name, rest[1:-1]) out.append(f"{indent}{name} = {rewritten}{newline}") index += 1 toml_path.write_bytes("".join(out).encode("utf-8")) PY } src_compile() { export CRAFT_FONTS_DIR=/usr/share/craft-fonts export CRAFT_FONTS_REQUIRED=1 local -a pkgs=() use gui && pkgs+=( -p designcraft ) use cli && pkgs+=( -p designcraft-cli ) cargo_src_compile "${pkgs[@]}" } src_test() { # cargo_src_test would forward a package selection and drop the library crates. cargo_src_test --workspace --exclude designcraft-web --exclude xtask } src_install() { # cargo install rejects -p. src_compile already built the selected bins. use gui && dobin "$(cargo_target_dir)/designcraft" use cli && dobin "$(cargo_target_dir)/designcraft-cli" use gui && QA_FLAGS_IGNORED+=" usr/bin/designcraft" use cli && QA_FLAGS_IGNORED+=" usr/bin/designcraft-cli" if use gui; then domenu packaging/linux/ai.storyteller.designcraft.desktop insinto /usr/share/mime/packages newins packaging/linux/ai.storyteller.designcraft.mime.xml \ ai.storyteller.designcraft.xml local rel_date if [[ -n ${SOURCE_DATE_EPOCH} ]]; then rel_date=$(date -u -d "@${SOURCE_DATE_EPOCH}" +%Y-%m-%d) || die else rel_date=$(date -u +%Y-%m-%d) || die fi sed -e "s/@VERSION@/${PV}/g" -e "s/@DATE@/${rel_date}/g" \ packaging/linux/ai.storyteller.designcraft.metainfo.xml.in \ > "${T}"/ai.storyteller.designcraft.metainfo.xml || die insinto /usr/share/metainfo doins "${T}"/ai.storyteller.designcraft.metainfo.xml local sz for sz in 16 24 32 48 64 128 256 512; do doicon -s "${sz}" \ "assets/app-icon/hicolor/${sz}x${sz}/apps/ai.storyteller.designcraft.png" done if [[ -f assets/app-icon/hicolor/scalable/apps/ai.storyteller.designcraft.svg ]]; then doicon -s scalable \ assets/app-icon/hicolor/scalable/apps/ai.storyteller.designcraft.svg fi fi }