11#!/usr/bin/env python3
22from __future__ import annotations
33
4- """Build release artifacts for the Zotero attachment plugin."""
4+ """Build release artifacts for the Zotero Local Write API plugin."""
55
66import hashlib
77import json
88import re
99import zipfile
1010from pathlib import Path
1111
12- from version import (
13- ADDON_AUTHOR ,
14- ADDON_DESCRIPTION ,
15- ADDON_ID ,
16- ADDON_NAME ,
17- FULLTEXT_ATTACH_PATH ,
18- LOCAL_WRITE_PATH ,
19- REPO_URL ,
20- STRICT_MIN_VERSION ,
21- STRICT_MAX_VERSION ,
22- TESTED_ZOTERO_VERSION ,
23- UPDATE_MANIFEST_FILENAME ,
24- UPDATE_MANIFEST_URL ,
25- VERSION ,
26- VERSION_PATH ,
27- XPI_FILENAME ,
28- XPI_URL ,
29- )
30-
12+ import yaml
3113
3214ROOT = Path (__file__ ).resolve ().parent
33- BOOTSTRAP_PATH = ROOT / "bootstrap.js"
34- MANIFEST_PATH = ROOT / "manifest.json"
35- UPDATES_PATH = ROOT / UPDATE_MANIFEST_FILENAME
15+ SRC = ROOT / "src"
16+ BOOTSTRAP_PATH = SRC / "bootstrap.js"
17+ ICONS_DIR = SRC / "icons"
18+ UPDATES_PATH = ROOT / "updates.json"
19+
20+ cfg = yaml .safe_load ((ROOT / "config.yml" ).read_text ())
21+ VERSION = (ROOT / "VERSION" ).read_text ().strip ()
22+
23+ ADDON_ID = cfg ["addon" ]["id" ]
24+ ADDON_SLUG = cfg ["addon" ]["slug" ]
25+ ADDON_NAME = cfg ["addon" ]["name" ]
26+ ADDON_AUTHOR = cfg ["addon" ]["author" ]
27+ ADDON_DESCRIPTION = cfg ["addon" ]["description" ]
28+
29+ REPO_OWNER = cfg ["repo" ]["owner" ]
30+ REPO_NAME = cfg ["repo" ]["name" ]
31+ REPO_BRANCH = cfg ["repo" ]["branch" ]
32+ REPO_URL = f"https://github.com/{ REPO_OWNER } /{ REPO_NAME } "
33+
34+ STRICT_MIN_VERSION = cfg ["zotero" ]["strict_min_version" ]
35+ STRICT_MAX_VERSION = cfg ["zotero" ]["strict_max_version" ]
36+ TESTED_ZOTERO_VERSION = cfg ["zotero" ]["tested_version" ]
37+
38+ ATTACH_PATH = cfg ["endpoints" ]["attach" ]
39+ WRITE_PATH = cfg ["endpoints" ]["write" ]
40+ VERSION_PATH = cfg ["endpoints" ]["version" ]
41+
42+ UPDATE_MANIFEST_URL = (
43+ f"https://raw.githubusercontent.com/{ REPO_OWNER } /{ REPO_NAME } /{ REPO_BRANCH } /updates.json"
44+ )
45+ XPI_FILENAME = f"{ ADDON_SLUG } -{ VERSION } .xpi"
46+ XPI_URL = f"https://github.com/{ REPO_OWNER } /{ REPO_NAME } /releases/download/v{ VERSION } /{ XPI_FILENAME } "
47+
3648BOOTSTRAP_VAR_PATTERNS = {
37- "PLUGIN_VERSION" : re .compile (r' var PLUGIN_VERSION = .*?;' ),
38- "FULLTEXT_ATTACH_PATH" : re .compile (r' var FULLTEXT_ATTACH_PATH = .*?;' ),
39- "LOCAL_WRITE_PATH" : re .compile (r' var LOCAL_WRITE_PATH = .*?;' ),
40- "VERSION_PATH" : re .compile (r' var VERSION_PATH = .*?;' ),
41- "ADDON_ID" : re .compile (r' var ADDON_ID = .*?;' ),
42- "HOMEPAGE_URL" : re .compile (r' var HOMEPAGE_URL = .*?;' ),
43- "UPDATE_URL" : re .compile (r' var UPDATE_URL = .*?;' ),
44- "STRICT_MIN_VERSION" : re .compile (r' var STRICT_MIN_VERSION = .*?;' ),
45- "STRICT_MAX_VERSION" : re .compile (r' var STRICT_MAX_VERSION = .*?;' ),
46- "TESTED_ZOTERO_VERSION" : re .compile (r' var TESTED_ZOTERO_VERSION = .*?;' ),
49+ "PLUGIN_VERSION" : re .compile (r" var PLUGIN_VERSION = .*?;" ),
50+ "FULLTEXT_ATTACH_PATH" : re .compile (r" var FULLTEXT_ATTACH_PATH = .*?;" ),
51+ "LOCAL_WRITE_PATH" : re .compile (r" var LOCAL_WRITE_PATH = .*?;" ),
52+ "VERSION_PATH" : re .compile (r" var VERSION_PATH = .*?;" ),
53+ "ADDON_ID" : re .compile (r" var ADDON_ID = .*?;" ),
54+ "HOMEPAGE_URL" : re .compile (r" var HOMEPAGE_URL = .*?;" ),
55+ "UPDATE_URL" : re .compile (r" var UPDATE_URL = .*?;" ),
56+ "STRICT_MIN_VERSION" : re .compile (r" var STRICT_MIN_VERSION = .*?;" ),
57+ "STRICT_MAX_VERSION" : re .compile (r" var STRICT_MAX_VERSION = .*?;" ),
58+ "TESTED_ZOTERO_VERSION" : re .compile (r" var TESTED_ZOTERO_VERSION = .*?;" ),
4759}
4860BOOTSTRAP_VAR_VALUES = {
4961 "PLUGIN_VERSION" : VERSION ,
50- "FULLTEXT_ATTACH_PATH" : FULLTEXT_ATTACH_PATH ,
51- "LOCAL_WRITE_PATH" : LOCAL_WRITE_PATH ,
62+ "FULLTEXT_ATTACH_PATH" : ATTACH_PATH ,
63+ "LOCAL_WRITE_PATH" : WRITE_PATH ,
5264 "VERSION_PATH" : VERSION_PATH ,
5365 "ADDON_ID" : ADDON_ID ,
5466 "HOMEPAGE_URL" : REPO_URL ,
5971}
6072
6173
74+ def write_json (path : Path , payload : dict [str , object ]) -> None :
75+ path .write_text (f"{ json .dumps (payload , indent = 2 , sort_keys = True )} \n " )
76+
77+
78+ def update_bootstrap_metadata () -> None :
79+ source = BOOTSTRAP_PATH .read_text ()
80+ for var , pattern in BOOTSTRAP_VAR_PATTERNS .items ():
81+ source , n = pattern .subn (
82+ f"var { var } = { json .dumps (BOOTSTRAP_VAR_VALUES [var ])} ;" ,
83+ source ,
84+ count = 1 ,
85+ )
86+ if n != 1 :
87+ raise RuntimeError (f"Could not update { var } in bootstrap.js" )
88+ BOOTSTRAP_PATH .write_text (source )
89+
90+
6291def build_manifest () -> dict [str , object ]:
6392 return {
6493 "manifest_version" : 2 ,
@@ -79,29 +108,6 @@ def build_manifest() -> dict[str, object]:
79108 }
80109
81110
82- def write_json (path : Path , payload : dict [str , object ]) -> None :
83- path .write_text (f"{ json .dumps (payload , indent = 2 , sort_keys = True )} \n " )
84-
85-
86- def update_bootstrap_metadata () -> None :
87- bootstrap_source = BOOTSTRAP_PATH .read_text ()
88- updated_source = bootstrap_source
89- for variable_name , pattern in BOOTSTRAP_VAR_PATTERNS .items ():
90- updated_source , replacements = pattern .subn (
91- f"var { variable_name } = { json .dumps (BOOTSTRAP_VAR_VALUES [variable_name ])} ;" ,
92- updated_source ,
93- count = 1 ,
94- )
95- if replacements != 1 :
96- raise RuntimeError (f"Could not update { variable_name } in bootstrap.js" )
97- BOOTSTRAP_PATH .write_text (updated_source )
98-
99-
100- def remove_old_xpis () -> None :
101- for old_xpi in ROOT .glob ("*.xpi" ):
102- old_xpi .unlink ()
103-
104-
105111_EPOCH = (2020 , 1 , 1 , 0 , 0 , 0 ) # fixed timestamp for deterministic builds
106112
107113
@@ -113,13 +119,13 @@ def _zip_entry(arcname: str) -> zipfile.ZipInfo:
113119
114120
115121def build_xpi () -> Path :
122+ manifest_path = SRC / "manifest.json"
116123 xpi_path = ROOT / XPI_FILENAME
117- icons_dir = ROOT / "icons"
118124 with zipfile .ZipFile (xpi_path , "w" , zipfile .ZIP_DEFLATED ) as xpi :
119- for path , arcname in [( MANIFEST_PATH , MANIFEST_PATH . name ), ( BOOTSTRAP_PATH , BOOTSTRAP_PATH . name )]:
120- xpi .writestr (_zip_entry (arcname ), path .read_bytes ())
121- if icons_dir .is_dir ():
122- for icon in sorted (icons_dir .iterdir ()):
125+ xpi . writestr ( _zip_entry ( "manifest.json" ), manifest_path . read_bytes ())
126+ xpi .writestr (_zip_entry ("bootstrap.js" ), BOOTSTRAP_PATH .read_bytes ())
127+ if ICONS_DIR .is_dir ():
128+ for icon in sorted (ICONS_DIR .iterdir ()):
123129 if icon .is_file ():
124130 xpi .writestr (_zip_entry (f"icons/{ icon .name } " ), icon .read_bytes ())
125131 return xpi_path
@@ -151,22 +157,27 @@ def build_updates_manifest(xpi_hash: str) -> dict[str, object]:
151157 }
152158
153159
160+ def remove_old_xpis () -> None :
161+ for old_xpi in ROOT .glob ("*.xpi" ):
162+ old_xpi .unlink ()
163+
164+
154165def build () -> Path :
155166 print (f"Building { ADDON_NAME } v{ VERSION } " )
156- print (f"Zotero compatibility: { STRICT_MIN_VERSION } - { STRICT_MAX_VERSION } " )
157- print (f"Tested target for release gating : Zotero { TESTED_ZOTERO_VERSION } " )
167+ print (f"Zotero compatibility: { STRICT_MIN_VERSION } – { STRICT_MAX_VERSION } " )
168+ print (f"Tested target: Zotero { TESTED_ZOTERO_VERSION } " )
158169
159170 update_bootstrap_metadata ()
160- write_json (MANIFEST_PATH , build_manifest ())
171+ manifest = build_manifest ()
172+ write_json (SRC / "manifest.json" , manifest )
161173 remove_old_xpis ()
162174 xpi_path = build_xpi ()
163175 write_json (UPDATES_PATH , build_updates_manifest (sha256sum (xpi_path )))
164176
165- print (f"Wrote { MANIFEST_PATH .name } " )
166- print (f"Wrote { UPDATES_PATH .name } " )
177+ print (f"Wrote updates.json" )
167178 print (f"Built { xpi_path .name } " )
168- print (f"Published update manifest URL: { UPDATE_MANIFEST_URL } " )
169- print (f"Published XPI URL: { XPI_URL } " )
179+ print (f"Update manifest URL: { UPDATE_MANIFEST_URL } " )
180+ print (f"XPI URL: { XPI_URL } " )
170181 return xpi_path
171182
172183
0 commit comments