"""Import the supplied 0917 shop slices and author the separate Creator 2.4 prefab. Run with a Pillow-enabled Python. Existing asset UUIDs are deterministic. Artwork is copied unchanged; missing shared slices are extracted losslessly from their atlas. """ import argparse import copy import json import shutil import uuid from pathlib import Path from PIL import Image ROOT = Path(__file__).resolve().parents[1] DEST = ROOT / "assets/shop/redesign0917" NS = uuid.UUID("1c30e866-71dc-4b6d-9fb6-3c47a9b69817") # Source artwork keeps its supplied names; imported assets use English names. RESOURCE_NAMES = { "打折数字": "discount_digits", "个数数量": "quantity_digits", "钱数数字": "price_green_digits", "黄色钱数数字": "price_gold_digits", "首购数字": "bonus_digits", "商城月卡存图0917": "monthly_card", "剩余天数数字": "day_digits", "chuizi00.png": "hammer.png", "lijilq.png": "claim_button.png", "mofab00.png": "magic_wand.png", "shijian00.png": "freeze.png", "shop_1.png": "header_background.png", "tu00.png": "gift_coin_icon.png", "wenzi00.png": "gift_title_small.png", "xianshitub.png": "limited_badge.png", "元.png": "currency_yuan.png", "加号.png": "bonus_plus.png", "商城底板.png": "shop_background.png", "图标00.png": "infinite_health.png", "小圆点00.png": "dot_normal.png", "小圆点01.png": "dot_selected.png", "小鱼干小兔.png": "starter_banner.png", "幸运礼包小图.png": "lucky_banner.png", "底框.png": "gift_card.png", "打折标签.png": "discount_tag.png", "文字01.png": "gift_title_medium.png", "文字大.png": "gift_title_large.png", "标签00.png": "first_purchase_badge.png", "礼包01.png": "gift_small.png", "礼包02.png": "gift_medium.png", "礼包03.png": "gift_large.png", "礼包条.png": "gift_section_title.png", "立即购买按.png": "buy_button.png", "超值tu.png": "value_badge.png", "金币图底图.png": "coin_card.png", "金币条.png": "coin_section_title.png", "钱数按钮.png": "coin_price_button.png", "钱数按钮00.png": "gift_price_button.png", "首购小.png": "first_purchase_special_badge.png", "30天特权字.png": "privilege_30_days.png", "剩余天数.png": "remaining_days.png", "月卡小图.png": "month_banner.png", "立即开通.png": "activate_button.png", "立即续费.png": "renew_button.png", **{f"金币0{i}.png": f"coin_0{i}.png" for i in range(1, 7)}, } def english_path(path): result = Path(*[RESOURCE_NAMES.get(part.strip(), part.strip()) for part in Path(path).parts]) if not result.as_posix().isascii(): raise ValueError(f"Missing English asset name: {path}") return result def uid(key): return str(uuid.uuid5(NS, key)) def compressed_uuid(value): digits = value.replace("-", "") alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" result = digits[:5] for i in range(5, 32, 3): n = int(digits[i:i+3], 16) result += alphabet[n >> 6] + alphabet[n & 63] return result def write_json(path, data): path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") def folder(path): path.mkdir(parents=True, exist_ok=True) meta_path = Path(str(path) + ".meta") existing = json.loads(meta_path.read_text(encoding="utf-8")) if meta_path.exists() else {} write_json(meta_path, dict(ver="1.1.3", uuid=existing.get("uuid", uid(str(path.relative_to(ROOT)))), importer="folder", isBundle=False, bundleName="", priority=1, compressionType={}, optimizeHotUpdate={}, inlineSpriteFrames={}, isRemoteBundle={}, subMetas={})) def texture(path): w, h = Image.open(path).size key = path.relative_to(ROOT).as_posix() tex, frame = uid(key), uid(key + "/frame") meta_path = Path(str(path) + ".meta") if meta_path.exists(): existing = json.loads(meta_path.read_text(encoding="utf-8")) tex = existing["uuid"] frame = next(iter(existing["subMetas"].values()))["uuid"] sub = dict(ver="1.0.6", uuid=frame, importer="sprite-frame", rawTextureUuid=tex, trimType="none", trimThreshold=1, rotated=False, offsetX=0, offsetY=0, trimX=0, trimY=0, width=w, height=h, rawWidth=w, rawHeight=h, borderTop=0, borderBottom=0, borderLeft=0, borderRight=0, subMetas={}) write_json(Path(str(path) + ".meta"), dict(ver="2.3.7", uuid=tex, importer="texture", type="sprite", wrapMode="clamp", filterMode="bilinear", premultiplyAlpha=False, genMipmaps=False, packable=w <= 1024 and h <= 1024, width=w, height=h, platformSettings={}, subMetas={path.stem: sub})) return frame, w, h def trimmed_sprites(data, frame_sizes): """Keep authored layout nodes; native-size TRIMMED images are scaled leaves. Children and widgets stay on the layout node, so changing image size mode cannot resize its buttons, displace text, or scale the entire child hierarchy. """ # Creator's built-in 2x2 splash frame is used by the payment overlay masks. frame_sizes = {"a23235d1-15db-4b95-8439-a2e005bfff91": (2, 2), **frame_sizes} for i, sprite in list(enumerate(data)): if sprite["__type__"] != "cc.Sprite": continue owner_id = sprite["node"]["__id__"] owner = data[owner_id] if owner["_name"] == "SpriteVisual": sprite["_sizeMode"] = 1 continue frame = sprite.get("_spriteFrame") w, h = frame_sizes[frame["__uuid__"]] if frame else (owner["_contentSize"]["width"], owner["_contentSize"]["height"]) visual = copy.deepcopy(owner) visual.update(_name="SpriteVisual", _parent={"__id__": owner_id}, _children=[], _components=[{"__id__": i}], _prefab=None, _opacity=255, _active=True, _id="", _skewX=0, _skewY=0) visual["_contentSize"].update(width=w, height=h) visual["_anchorPoint"].update(x=.5, y=.5) visual["_eulerAngles"].update(x=0, y=0, z=0) size, anchor = owner["_contentSize"], owner["_anchorPoint"] visual["_trs"]["array"] = [(0.5-anchor["x"])*size["width"], (0.5-anchor["y"])*size["height"], 0, 0, 0, 0, 1, size["width"]/w if w else 1, size["height"]/h if h else 1, 1] visual_id = len(data) data.append(visual) owner["_components"].remove({"__id__": i}) owner["_children"].insert(0, {"__id__": visual_id}) sprite.update(node={"__id__": visual_id}, _sizeMode=1) def asset_index(): frames, textures = {}, {} for p in (ROOT / "assets").rglob("*.meta"): if DEST in p.parents: continue try: m = json.loads(p.read_text(encoding="utf-8-sig")) except (ValueError, OSError): continue if m.get("importer") == "texture": textures[m["uuid"]] = Path(str(p)[:-5]) for name, f in m.get("subMetas", {}).items(): if f.get("importer") == "sprite-frame": frames[f["uuid"]] = (p, name, f) return frames, textures def extract(frame, textures): im = Image.open(textures[frame["rawTextureUuid"]]).convert("RGBA") x, y, w, h = [frame[k] for k in ("trimX", "trimY", "width", "height")] im = im.crop((x, y, x + (h if frame["rotated"] else w), y + (w if frame["rotated"] else h))) if frame["rotated"]: im = im.transpose(Image.Transpose.ROTATE_90) result = Image.new("RGBA", (frame["rawWidth"], frame["rawHeight"])) result.paste(im, (round((result.width-w)/2+frame["offsetX"]), round((result.height-h)/2-frame["offsetY"]))) return result def build(source): folder(DEST) folder(DEST / "texture") folder(DEST / "texture/shared") frames, textures = asset_index() art, provenance = {}, {} for p in sorted(source.rglob("*.png")): rel = Path(*[part.strip() for part in p.relative_to(source).parts]) imported = english_path(rel) dst = DEST / "texture" / imported if not dst.parent.exists(): folder(dst.parent) shutil.copyfile(p, dst) art[rel.as_posix()] = texture(dst) provenance[imported.as_posix()] = str(p) old = json.loads((ROOT / "assets/shop/prefab/shop.prefab").read_text(encoding="utf-8")) copied = {} def shared(frame_uuid, name=None): if frame_uuid in copied: return copied[frame_uuid] meta, original, f = frames[frame_uuid] name = name or (Path(original).stem + "_" + frame_uuid[:8]) rel = "shared/" + name + ".png" dst = DEST / "texture" / rel extract(f, textures).save(dst) art[rel] = texture(dst) provenance[rel] = f"{meta.relative_to(ROOT).as_posix()} :: {original}" copied[frame_uuid] = rel return rel def named(atlas, name, output): f = json.loads((ROOT / atlas).read_text(encoding="utf-8"))["subMetas"][name] return shared(f["uuid"], output) hour = [named("assets/shop/img/texture_atlas-1.plist.meta", f"{n}h.png", f"hours_{n}") for n in (1, 2, 4)] multiply = shared("50d5f850-fb89-461d-9f80-becccda30060", "multiply") yellow_yuan = shared("1375b028-68b3-484a-972b-32553c1269f6", "yuan_yellow") hp = [named("assets/common/font.plist.meta", f"hp_{n}.png", f"hp_{n}") for n in range(10)] time = [named("assets/common/font.plist.meta", f"time_{n}.png", f"time_{n}") for n in range(10)] colon = named("assets/common/font.plist.meta", "time_10.png", "time_colon") data = [dict(__type__="cc.Prefab", _name="shop0917", _objFlags=0, _native="", data={"__id__": 1}, optimizationPolicy=0, asyncLoadAssets=False, readonly=False)] ref = lambda i: {"__id__": i} asset = lambda key: {"__uuid__": art[key][0]} templates = {t: next(x for x in old if x["__type__"] == t) for t in ("cc.Node", "cc.Sprite", "cc.Button", "cc.Widget", "cc.Mask", "cc.ScrollView")} def component(node, kind, **props): c = copy.deepcopy(templates.get(kind, dict(__type__=kind, _name="", _objFlags=0, _enabled=True, _id=""))) c.update(node=ref(node), **props) i = len(data) data.append(c) data[node]["_components"].append(ref(i)) return i def node(name, parent, x=0, y=0, w=0, h=0, key=None, active=True, anchor_y=.5): n = copy.deepcopy(templates["cc.Node"]) n.update(_name=name, _parent=ref(parent) if parent else None, _children=[], _components=[], _active=active, _prefab=None, _opacity=255, _id="") n["_trs"]["array"] = [x, y, 0, 0, 0, 0, 1, 1, 1, 1] n["_contentSize"].update(width=w, height=h) n["_anchorPoint"].update(x=.5, y=anchor_y) i = len(data) data.append(n) if parent: data[parent]["_children"].append(ref(i)) if key: component(i, "cc.Sprite", _spriteFrame=asset(key)) return i def image(name, parent, x, y, key, w=None, h=None): _, iw, ih = art[key] return node(name, parent, x, y, w or iw, h or ih, key) def widget(n, flags=45, top=0, bottom=0): component(n, "cc.Widget", _alignFlags=flags, _top=top, _bottom=bottom, _left=0, _right=0) def button(n, handler, custom="", script="shop"): e = len(data) data.append(dict(__type__="cc.ClickEvent", target=ref(1), component=script, _componentId="", handler=handler, customEventData=custom)) component(n, "cc.Button", clickEvents=[ref(e)], zoomScale=.97, **{"_N$target": ref(n)}) def number(name, parent, x, y, value, group, height, gap=-2, suffix=None): keys = [f"{group}/{c}.png" for c in str(value)] if suffix: keys.append(suffix) widths = [art[k][1]*height/art[k][2] for k in keys] total = sum(widths) + gap*(len(keys)-1) n = node(name, parent, x, y, total, height) cursor = -total/2 for i, (key, width) in enumerate(zip(keys, widths)): image(f"glyph_{i}", n, cursor+width/2, 0, key, width, height) cursor += width+gap return n def clone_tree(old_id, parent): # Clone an existing shared avatar/loading/dialog subtree, preserving local # references while copying all its visible image slices into the new folder. ids = set() def visit(i): if i in ids: return ids.add(i) obj = old[i] if obj["__type__"] == "cc.Node": for r in obj["_children"] + obj["_components"]: visit(r["__id__"]) for r in obj.get("clickEvents", []): visit(r["__id__"]) visit(old_id) mapping = {i: len(data)+j for j, i in enumerate(sorted(ids))} def remap(v): if isinstance(v, dict): if "__id__" in v: return ref(mapping[v["__id__"]]) if v["__id__"] in mapping else (ref(1) if v["__id__"] == 1 else None) if "__uuid__" in v and v["__uuid__"] in frames: return asset(shared(v["__uuid__"])) return {k: remap(a) for k, a in v.items()} if isinstance(v, list): return [remap(a) for a in v] return v for i in sorted(ids): obj = remap(copy.deepcopy(old[i])) if obj["__type__"] == "cc.Node": obj["_prefab"] = None data.append(obj) result = mapping[old_id] data[result]["_parent"] = ref(parent) data[parent]["_children"].append(ref(result)) return result # Existing delivery/month-card callbacks locate Canvas/shop by name. root = node("shop", None, 540, 1170, 1080, 2340) widget(root) component(root, "cc.BlockInputEvents") background = image("Background", root, 0, 0, "商城底板.png", 1080, 2340) widget(background) # Only the product area scrolls. The first 1005 reference pixels stay fixed. scroll = node("ProductsScrollView", root, 0, -502.5, 1080, 1335) widget(scroll, top=1005) view = node("view", scroll, 0, 0, 1080, 1335) widget(view) component(view, "cc.Mask") content = node("content", view, 0, 667.5, 1080, 2855, anchor_y=1) scroll_comp = component(scroll, "cc.ScrollView", **{"_N$content": ref(content), "content": ref(content), "_N$verticalScrollBar": None, "elastic": True, "brake": .75}) image("ProductBackground", content, 0, -1427.5, "商城底板.png", 1080, 2855) image("GiftSectionTitle", content, 0, -69, "礼包条.png") for i, (price, coins, discount, title, hours, count) in enumerate([ (10, 2500, "7", "wenzi00.png", 1, 0), (20, 5000, "55", "文字01.png", 2, 2), (30, 7500, "4", "文字大.png", 4, 5), ]): # Reference card tops: 1143, 1635, 2135 (relative to full reference image). cy = -(138 + [0, 492, 992][i] + 226) card = image(f"Gift_{price}", content, 0, cy, "底框.png") image("GiftTitle", card, -252, -149, title) image("UnlimitedHealth", card, -329, 58 if i == 0 else 112, "图标00.png") image("Duration", card, -329, -1 if i == 0 else 52, hour[i]) image("CoinsIcon", card, -146, 62 if i == 0 else 110, "tu00.png") number("CoinAmount", card, -146, -4 if i == 0 else 49, coins, "个数数量", 46) if count: for j, key in enumerate(["chuizi00.png", "shijian00.png", "mofab00.png"]): px = -365+j*120 image("PropIcon"+str(j), card, px, -25, key, art[key][1]*.85, art[key][2]*.85) amount = node("PropAmount"+str(j), card, px, -87, 70, 40) image("multiply", amount, -20, 0, multiply, 25, 25) number("count", amount, 13, 0, count, "个数数量", 42) tag = image("DiscountTag", card, 32, 154, "打折标签.png") digits = number("Discount", tag, -9 if len(discount) == 1 else -22, 14, discount, "打折数字", 52) # The tag art itself is slanted; its digits follow that printed baseline. import math data[digits]["_trs"]["array"][5:7] = [math.sin(math.radians(-13)/2), math.cos(math.radians(-13)/2)] data[digits]["_eulerAngles"]["z"] = -13 gift_key = f"礼包0{i+1}.png" image("GiftArtwork", card, 259, 48, gift_key) buy = image("Buy", card, 265, -136, "钱数按钮00.png") button(buy, "buyProduct", f"unlimited_health_bundle_{price}") number("Price", buy, 0, 7, price, "黄色钱数数字", 65, 4, yellow_yuan) image("CoinSectionTitle", content, 0, -1689, "金币条.png") offers = [] for i, (coins, price) in enumerate(zip([1200, 8000, 16000, 32000, 100000, 240000], [6, 36, 68, 128, 328, 648])): card = image(f"gold_{i+1}", content, -330+(i%3)*330, -(1988+(i//3)*494), "金币图底图.png") offer = node("FirstPurchase", card) image("FirstPurchaseBadge", offer, -116, 149, "标签00.png") amount = number("BonusAmount", offer, 43, 153, coins, "首购数字", 41) image("Plus", amount, -data[amount]["_contentSize"]["width"]/2-16, 0, "加号.png", 28, 28) offers.append(offer) key = f"金币0{i+1}.png" image("CoinArtwork", card, 0, -10, key) number("CoinAmount", card, 0, -87, coins, "个数数量", 46) buy = image("Buy", card, 0, -181, "钱数按钮.png") button(buy, "buyProduct", f"gold_{i+1}") number("Price", buy, 0, 7, price, "钱数数字", 54, 1, "元.png") top = node("Top", root, 0, 667.5, 1080, 1005) widget(top, flags=41) image("HeaderBackground", top, 0, 0, "shop_1.png") for original, name in [(472, "kuang"), (477, "avatar")]: n = clone_tree(original, top) data[n]["_trs"]["array"][:2] = [-427, 185.5] # Avatar frame is decorative here; the profile page remains outside shop. data[n]["_components"] = [r for r in data[n]["_components"] if data[r["__id__"]]["__type__"] != "cc.Button"] close = image("Close", top, 436, 139.5, shared("4eb76a13-bdd5-44cb-975d-32fd4900be79", "close")) button(close, "closeShop", "exit") stamina = node("Stamina", top, -169, 224.5, 285, 100) image("bg", stamina, 26.654, 0, shared("27e4e784-2bb1-40b2-9726-a3d160165c43", "health_background"), 236, 62) progress = image("progresss", stamina, 26.654, 0, shared("21ec0fc2-b090-4eb7-bb39-83b6de6b2f87", "health_fill"), 238, 64) data[data[progress]["_components"][0]["__id__"]].update(_type=3, _fillRange=1) full = node("man", stamina) image("Full", full, 25, 0, shared("1eabaff0-07cb-4de7-a55d-75a068061ea2", "health_full"), 74, 38) image("Heart", stamina, -98, 0, shared("b6b570b9-d928-4646-8a2a-70f18cc10991", "heart"), 92, 86) plus = shared("f9ab818d-bcb7-465b-af54-0af12cf7d936", "plus_green") image("Plus", stamina, -69, -25, plus, 38, 38) health = node("health", stamina, -98, 0) image("glyph_0", health, 0, 0, hp[5], 30, 39) timer = node("time", stamina, 27, 0, active=False) skyline = node("skyLine", stamina, 0, 0, active=False) image("Infinity", skyline, -98, 0, shared("419e6dd8-8ab5-4910-bf94-b82afe18191b", "health_infinite"), 99, 87) skytime = node("skyTime", skyline, 27, 0) button(stamina, "openHealth", script="Shop0917View") coinbar = node("Coin", top, 129, 224.5, 275, 100) image("Background", coinbar, 5, 0, shared("e29788dd-4216-4e8b-9186-25040fc8e3ec", "coin_background"), 238, 64) image("Icon", coinbar, -105, 0, shared("94a3bed8-306b-4f18-9806-2df14cc935f3", "coin_icon"), 84, 95) image("Plus", coinbar, -69, -25, plus, 38, 38) coin = number("Coin", coinbar, 30, 0, 563, "个数数量", 37) button(coinbar, "scrollToCoins", script="Shop0917View") carousel = node("Carousel", top, 0, -247.5, 1080, 381) component(carousel, "cc.Mask") banners = [] # The starter_pack purchase opens NewbieGift (lucky gift); Jungle is the fish-box activity. for i, key in enumerate(["幸运礼包小图.png", "商城月卡存图0917/月卡小图.png", "小鱼干小兔.png"]): card = image(["StarterBanner", "MonthBanner", "LuckyBanner"][i], carousel, (i-1)*900+5, 0, key, 862, 381) banners.append(card) if i == 1: month_title = image("Privilege", card, 357, 121, "商城月卡存图0917/30天特权字.png") month_days = node("RemainingDays", card, 357, 121, active=False) image("Caption", month_days, 0, 0, "商城月卡存图0917/剩余天数.png") day_number = number("Days", month_days, 0, 0, 30, "商城月卡存图0917/剩余天数数字", 32) month_buy = image("Open", card, 254, -83, "商城月卡存图0917/立即开通.png") button(month_buy, "openmonthCard") else: image("Badge", card, 337, 110, "xianshitub.png" if i == 0 else "超值tu.png") buy = image("Open", card, 254, -83, "立即购买按.png" if i == 0 else "lijilq.png") button(buy, "openStarter" if i == 0 else "openJungleTreasure", script="Shop0917View" if i == 0 else "shop") dots = [] for i in range(3): dot = image("Dot"+str(i), top, (i-1)*47, -464.5, "小圆点01.png" if i == 1 else "小圆点00.png") button(dot, "selectBanner", str(i), "Shop0917View") dots.append(dot) # Shared payment overlays retain their existing behavior and are independent # of the supplied storefront design. The loading animation needs no font text. loading = clone_tree(560, root) data[loading]["_active"] = False data[loading]["_children"] = [r for r in data[loading]["_children"] if data[r["__id__"]]["_name"] != "New Label"] confirm = clone_tree(597, root) data[confirm]["_active"] = False # Serialize the controller with the same payment/lifecycle API as the old shop. controller = copy.deepcopy(old[624]) controller.update(node=ref(root), shop=ref(root), itemList=ref(content), coin=ref(coin), Stamina=ref(stamina), monthCardTime=ref(month_days), coinAnim=None) controller_id = len(data) data.append(controller) data[root]["_components"].append(ref(controller_id)) view_uuid = json.loads((ROOT / "assets/shop/script/Shop0917View.ts.meta").read_text())["uuid"] v = component(root, compressed_uuid(view_uuid), scrollView=ref(scroll_comp), carousel=ref(carousel), banners=list(map(ref, banners)), dots=list(map(ref, dots)), firstPurchase=list(map(ref, offers)), coin=ref(coin), health=ref(health), monthPrivilege=ref(month_title), monthRemaining=ref(month_days), monthDays=ref(day_number), monthButton=ref(data[month_buy]["_components"][0]["__id__"]), countFrames=[asset(f"个数数量/{i}.png") for i in range(10)], healthFrames=[asset(k) for k in hp], timeFrames=[asset(k) for k in time], colonFrame=asset(colon), dayFrames=[asset(f"商城月卡存图0917/剩余天数数字/{i}.png") for i in range(10)], monthOpen=asset("商城月卡存图0917/立即开通.png"), monthRenew=asset("商城月卡存图0917/立即续费.png"), dotNormal=asset("小圆点00.png"), dotSelected=asset("小圆点01.png")) controller["redesignView"] = ref(v) # Remove detached legacy objects and assign stable prefab file IDs to all nodes. live = set() def collect(i): if i in live: return live.add(i) obj = data[i] def scan(value): if isinstance(value, dict): if "__id__" in value: collect(value["__id__"]) else: for a in value.values(): scan(a) elif isinstance(value, list): for a in value: scan(a) scan(obj) collect(0) mapping = {i: j for j, i in enumerate(sorted(live))} def reindex(v): if isinstance(v, dict): return {"__id__": mapping[v["__id__"]]} if "__id__" in v else {k: reindex(a) for k,a in v.items()} return [reindex(a) for a in v] if isinstance(v, list) else v data = [reindex(data[i]) for i in sorted(live)] trimmed_sprites(data, {frame: (w, h) for frame, w, h in art.values()}) for i, obj in list(enumerate(data)): if obj["__type__"] == "cc.Node": obj["_prefab"] = ref(len(data)) data.append(dict(__type__="cc.PrefabInfo", root=ref(1), asset=ref(0), fileId=uid(f"node/{i}/{obj['_name']}").replace("-", ""), sync=False)) prefab = ROOT / "assets/shop/prefab/shop0917.prefab" write_json(prefab, data) write_json(Path(str(prefab)+".meta"), dict(ver="1.3.2", uuid=uid("shop0917.prefab"), importer="prefab", optimizationPolicy="MULTI_INSTANCE", asyncLoadAssets=False, readonly=False, subMetas={})) write_json(ROOT / "docs/shop0917-resource-manifest.json", provenance) print(f"Authored {prefab.relative_to(ROOT)}: {len(data)} objects, {len(art)} independent image slices") if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("source", type=Path) build(parser.parse_args().source)