1
+ #!/usr/bin/env python3
2
+ """KiCad PCM plugin version updater"""
3
+ import argparse
4
+ import json
5
+ import logging
6
+ import sys
7
+ import tempfile
8
+ import time
9
+ import typing
10
+ import urllib .error
11
+ import urllib .request
12
+ import zipfile
13
+ from datetime import datetime , timezone
14
+ from hashlib import sha256
15
+ from pathlib import Path
16
+
17
+ logging .basicConfig (format = '%(levelname)s: %(message)s' , level = logging .INFO )
18
+ log = logging .getLogger ('pcm-updater' )
19
+
20
+ def fatal (msg : str , exit_code : int = 1 ) -> typing .NoReturn :
21
+ log .error (msg )
22
+ sys .exit (exit_code )
23
+
24
+
25
+ def parse_arguments () -> argparse .Namespace :
26
+ parser = argparse .ArgumentParser (
27
+ description = "Add or update a plugin version in KiCad PCM index"
28
+ )
29
+ parser .add_argument ("identifier" , help = "Plugin identifier (e.g. com.github.username.plugin)" )
30
+ parser .add_argument ("version" , help = "Version of the plugin (e.g. 1.0.0)" )
31
+ parser .add_argument ("status" , choices = ["stable" , "testing" , "development" , "deprecated" ],
32
+ help = "Status of the version (stable, testing, development, deprecated)" )
33
+ parser .add_argument ("kicad_version" , help = "Minimum KiCad version required (e.g. 8.0)" )
34
+ parser .add_argument ("download_url" , help = "URL to download the plugin archive" )
35
+ return parser .parse_args ()
36
+
37
+
38
+ def download_and_calculate_metrics (url : str ) -> typing .Dict [str , typing .Any ]:
39
+ """Calculate download_size, download_sha256, and install_size metrics for plugin"""
40
+ log .info (f"Downloading plugin from { url } " )
41
+
42
+ with tempfile .TemporaryDirectory () as temp_dir :
43
+ temp_path = Path (temp_dir )
44
+ archive_path = temp_path / "plugin.zip"
45
+
46
+ try :
47
+ urllib .request .urlretrieve (url , archive_path )
48
+ except Exception as e :
49
+ fatal (f"Download error: { e } " )
50
+
51
+ if not archive_path .exists () or archive_path .stat ().st_size == 0 :
52
+ fatal ("Downloaded file is empty or does not exist" )
53
+
54
+ download_size = archive_path .stat ().st_size
55
+
56
+ file_hash = sha256 ()
57
+ with open (archive_path , "rb" ) as f :
58
+ for chunk in iter (lambda : f .read (4096 ), b"" ):
59
+ file_hash .update (chunk )
60
+ download_sha256 = file_hash .hexdigest ()
61
+
62
+ extract_dir = temp_path / "extracted"
63
+ extract_dir .mkdir (exist_ok = True )
64
+
65
+ try :
66
+ with zipfile .ZipFile (archive_path , 'r' ) as zip_ref :
67
+ zip_ref .extractall (extract_dir )
68
+ except Exception as e :
69
+ fatal (f"Error extracting ZIP archive: { e } " )
70
+
71
+ install_size = get_directory_size (extract_dir )
72
+ log .info (f"Plugin metrics - Size: { download_size } bytes, SHA256: { download_sha256 [:8 ]} ..." )
73
+
74
+ return {
75
+ "download_size" : download_size ,
76
+ "download_sha256" : download_sha256 ,
77
+ "install_size" : install_size
78
+ }
79
+
80
+
81
+ def get_directory_size (directory : Path ) -> int :
82
+ return sum (f .stat ().st_size for f in directory .glob ('**/*' ) if f .is_file ())
83
+
84
+
85
+ def update_packages_json (args , metrics : typing .Dict [str , typing .Any ]) -> None :
86
+ """Find plugin by identifier and add/update version information in packages.json"""
87
+ log .info ("Updating packages.json" )
88
+ packages_file = Path ("packages.json" )
89
+
90
+ if not packages_file .exists ():
91
+ fatal ("packages.json not found" )
92
+
93
+ try :
94
+ with open (packages_file , "r" ) as f :
95
+ packages_data = json .load (f )
96
+
97
+ if "packages" not in packages_data :
98
+ fatal ("Invalid packages.json structure - 'packages' key not found" )
99
+
100
+ plugin_found = False
101
+ for package in packages_data ["packages" ]:
102
+ if package ["identifier" ] == args .identifier :
103
+ plugin_found = True
104
+
105
+ new_version = {
106
+ "version" : args .version ,
107
+ "status" : args .status ,
108
+ "kicad_version" : args .kicad_version ,
109
+ "download_url" : args .download_url ,
110
+ "download_sha256" : metrics ["download_sha256" ],
111
+ "download_size" : metrics ["download_size" ],
112
+ "install_size" : metrics ["install_size" ]
113
+ }
114
+
115
+ for i , version in enumerate (package .get ("versions" , [])):
116
+ if version ["version" ] == args .version :
117
+ package ["versions" ][i ] = new_version
118
+ log .info (f"Updated existing version { args .version } " )
119
+ break
120
+ else :
121
+ if "versions" not in package :
122
+ package ["versions" ] = []
123
+ package ["versions" ].append (new_version )
124
+ log .info (f"Added new version { args .version } " )
125
+ break
126
+
127
+ if not plugin_found :
128
+ fatal (f"Plugin '{ args .identifier } ' not found" )
129
+
130
+ with open (packages_file , "w" ) as f :
131
+ json .dump (packages_data , f , indent = 2 , ensure_ascii = False )
132
+ except Exception as e :
133
+ fatal (f"Error updating packages.json: { e } " )
134
+
135
+
136
+ def update_repository_json () -> None :
137
+ """Update UTC time and Unix timestamp for packages and resources in repository.json"""
138
+ log .info ("Updating repository.json timestamps" )
139
+ repo_file = Path ("repository.json" )
140
+
141
+ if not repo_file .exists ():
142
+ fatal ("repository.json not found" )
143
+
144
+ try :
145
+ timestamp = int (time .time ())
146
+ time_utc = datetime .now (timezone .utc ).strftime ("%Y-%m-%d %H:%M:%S" )
147
+
148
+ with open (repo_file , "r" ) as f :
149
+ repo_data = json .load (f )
150
+
151
+ if "packages" not in repo_data or "resources" not in repo_data :
152
+ fatal ("Invalid repository.json structure - required sections missing" )
153
+
154
+ for section in ["packages" , "resources" ]:
155
+ repo_data [section ]["update_time_utc" ] = time_utc
156
+ repo_data [section ]["update_timestamp" ] = timestamp
157
+
158
+ with open (repo_file , "w" ) as f :
159
+ json .dump (repo_data , f , indent = 2 , ensure_ascii = False )
160
+ except Exception as e :
161
+ fatal (f"Error updating repository.json: { e } " )
162
+
163
+
164
+ def main () -> None :
165
+ try :
166
+ args = parse_arguments ()
167
+ metrics = download_and_calculate_metrics (args .download_url )
168
+ update_packages_json (args , metrics )
169
+ update_repository_json ()
170
+ log .info (f"Successfully updated plugin { args .identifier } to version { args .version } " )
171
+ except KeyboardInterrupt :
172
+ log .warning ("Operation cancelled" )
173
+ sys .exit (1 )
174
+ except Exception as e :
175
+ fatal (f"Unexpected error: { e } " )
176
+
177
+
178
+ if __name__ == "__main__" :
179
+ main ()
0 commit comments