-
-
Notifications
You must be signed in to change notification settings - Fork 988
/
wesnoth_addon_manager
executable file
·451 lines (412 loc) · 18.2 KB
/
wesnoth_addon_manager
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
#!/usr/bin/env python3
# encoding: utf-8
"""
add-on_manager.py -- a command-line client for the Wesnoth add-on server
This tool is mainly intendended for user-made content authors and maintainers.
It can be used to manage the WML content on the Wesnoth add-on server.
Available functions include listing, downloading, uploading, and deleting
add-ons.
"""
import sys, os.path, re, time, glob, shutil
from subprocess import Popen
import wesnoth.wmlparser3 as wmlparser
from wesnoth.campaignserver_client import CampaignClient
# This is the validation code for the -u arguments. It checks if the input path is valid
def valid_file_path(path):
if os.path.isdir(path) or os.path.isfile(path):
return path
else:
sys.stderr.write("No such file or directory: %s\n" % path)
sys.exit(1)
return None
if __name__ == "__main__":
import argparse
try:
import psyco
psyco.full()
except ImportError:
pass
argumentparser = argparse.ArgumentParser()
argumentparser.add_argument("-a", "--address", help="specify server address",
default="add-ons.wesnoth.org")
argumentparser.add_argument("--html",
help="Output a HTML overview into the given directory.",)
argumentparser.add_argument("-p", "--port",
help="specify server port or BfW version (%s)" % " or ".join(
[x[1] for x in CampaignClient.portmap]),
default=CampaignClient.portmap[0][0])
argumentparser.add_argument("-l", "--list", help="list available add-ons",
action="store_true",)
argumentparser.add_argument("-w", "--wml",
help="when listing add-ons, list the raw wml",
action="store_true",)
argumentparser.add_argument("-C", "--color",
help="use colored WML output",
action="store_true",)
argumentparser.add_argument("-c", "--campaigns-dir",
help="directory where add-ons are stored",
default=".")
argumentparser.add_argument("-d", "--download",
help="download the named add-on; " +
"name may be a Python regexp matched against all add-on names " +
"(specify the path where to put it with -c, " +
"current directory will be used by default)")
argumentparser.add_argument("-T", "--type",
help="Type of addons to download, e.g. 'era' or 'campaign'.")
argumentparser.add_argument("-t", "--tar",
help="When used together with --download, create tarballs of any " +
"downloaded addons and put into the specified directory.")
argumentparser.add_argument("--pbl", help="override standard PBL location")
argumentparser.add_argument("--pbl-key", action='append', nargs=2,
metavar=("KEY", "VALUE"),
help="When uploading, override KEY with VALUE in _server.pbl. " +
"No changes are written to disk, only the upload is affected. " +
"This option only makes sense with --upload.")
argumentparser.add_argument("-u", "--upload",
help="Upload an add-on. " +
"UPLOAD should be either the name of an add-on subdirectory," +
"(in which case the client looks for _server.pbl beneath it) " +
"or a path to the .pbl file (in which case the name of the " +
"add-on subdirectory is the name of the path with .pbl removed)", type=valid_file_path)
argumentparser.add_argument("-s", "--status",
help="Display the status of addons installed in the given " +
"directory.")
argumentparser.add_argument("-f", "--update",
help="Update all installed add-ons in the given directory. " +
"This works by comparing the _info.cfg file in each addon directory " +
"with the version on the server.")
argumentparser.add_argument("-V", "--verbose",
help="be even more verbose for everything",
action="store_true",)
argumentparser.add_argument("-r", "--remove", nargs=2,
metavar=("ADD-ON", "PASSPHRASE"),
help="remove the named add-on from the server")
argumentparser.add_argument("-R", "--raw-download",
action="store_true",
help="download as a binary WML packet")
argumentparser.add_argument("--url", help="When used with --html, " +
"a download link will be added for each campaign, with the given " +
"base URL.")
argumentparser.add_argument("--datadir", help="When used with --html, " +
"specifies the Wesnoth data dir where add-on icons will be copied " +
"from.")
argumentparser.add_argument("-U", "--unpack",
help="unpack the file UNPACK as a binary WML packet " +
"(specify the add-on path with -c)")
argumentparser.add_argument("--change-passphrase", nargs=3,
metavar=("ADD-ON","OLD","NEW"),
help="Change the passphrase for ADD-ON from OLD to NEW")
args = argumentparser.parse_args()
port = args.port
if not args.port.isdigit():
for (portnum, version) in CampaignClient.portmap:
if args.port == version:
port = portnum
break
else:
sys.stderr.write("Unknown BfW version %s\n" % args.port)
sys.exit(1)
address = args.address
if not ":" in address:
address += ":" + str(port)
def get(name, title, version, type, uploads, dependencies, cdir):
mythread = cs.get_campaign_raw_async(name)
pcounter = 0
while not mythread.event.isSet():
mythread.event.wait(1)
if pcounter != cs.counter:
print("%s: %d/%d" % (name, cs.counter, cs.length))
pcounter = cs.counter
if args.raw_download:
file(name, "w").write(mythread.data)
else:
decoded = cs.decode(mythread.data)
dirname = os.path.join(cdir, name)
oldcfg_path = os.path.join(cdir, name + ".cfg")
# Try to remove old campaign in case it exists.
shutil.rmtree(dirname, True)
try: os.remove(oldcfg_path)
except OSError: pass
print("Unpacking %s..." % name)
cs.unpackdir(decoded, cdir, verbose=args.verbose)
info = os.path.join(dirname, "_info.cfg")
try:
f = open(info, "w")
infowml = """#
# File automatically generated by Wesnoth to keep track
# of version information on installed add-ons. DO NOT EDIT!
#
[info]
\tdependencies="%s"
\ttitle="%s"
\ttype="%s"
\tuploads=%s
\tversion="%s"
[/info]
"""
f.write(infowml %
(dependencies, title, type, uploads, version))
f.close()
except OSError:
pass
print_messages(decoded)
if args.tar:
try: os.mkdir(args.tar)
except OSError: pass
tarname = args.tar + "/" + name + ".tar.bz2"
if os.path.isfile(oldcfg_path):
oldcfg = name + ".cfg"
if args.verbose:
sys.stderr.write("Creating tarball with command: tar " +
"cjf %(tarname)s -C %(cdir)s %(name)s %(oldcfg)s\n" %
locals())
Popen(["tar", "cjf", tarname, "-C", cdir, name, oldcfg])
else:
if args.verbose:
sys.stderr.write("Creating tarball with command: tar " +
"cjf %(tarname)s -C %(cdir)s %(name)s\n" % locals())
Popen(["tar", "cjf", tarname, "-C", cdir, name])
def print_messages(data):
for message in data.get_all(tag = "message") + data.get_all(tag = "error"):
print(message.get_text_val("message"))
def parse_wml_file(name):
p = wmlparser.Parser()
p.parse_file(name)
return p.root
def parse_wml_text(text):
p = wmlparser.Parser()
p.parse_text(text)
return p.root
def get_info(name):
"""
Get info for a locally installed add-on. It expects a direct path
to the _info.cfg file.
"""
if not os.path.exists(name):
return None, None
info = parse_wml_file(name)
uploads = info.get_all(tag = "info")[0].get_text_val("uploads", "")
version = info.get_all(tag = "info")[0].get_text_val("version", "")
return uploads, version
campaign_list = None
if args.list:
cs = CampaignClient(address)
campaign_list = data = cs.list_campaigns()
if data:
campaigns = data.get_all(tag = "campaigns")[0]
if args.wml:
for campaign in campaigns.get_all(tag = "campaign"):
print(campaign.debug())
else:
column_sizes = [10, 5, 10, 7, 8, 8, 10, 5, 10, 13]
columns = [["type", "name", "title", "author",
"version", "uploads", "downloads",
"size", "timestamp", "translate"]]
for campaign in campaigns.get_all(tag = "campaign"):
column = [
campaign.get_text_val("type", "?"),
campaign.get_text_val("name", "?"),
campaign.get_text_val("title", "?"),
campaign.get_text_val("author", "?"),
campaign.get_text_val("version", "?"),
campaign.get_text_val("uploads", "?"),
campaign.get_text_val("downloads", "?"),
campaign.get_text_val("size", "?"),
time.ctime(int(campaign.get_text_val("timestamp", "0"))),
campaign.get_text_val("translate", "?")]
columns.append(column)
for i, s in enumerate(column_sizes):
if 1 + len(column[i]) > s:
column_sizes[i] = 1 + len(column[i])
for c in columns:
for i, f in enumerate(c):
sys.stdout.write(f.ljust(column_sizes[i]))
sys.stdout.write("\n")
print_messages(data)
else:
sys.stderr.write("Could not connect.\n")
elif args.download:
cs = CampaignClient(address)
fetchlist = []
campaign_list = data = cs.list_campaigns()
if data:
campaigns = data.get_all(tag = "campaigns")[0]
for campaign in campaigns.get_all(tag = "campaign"):
name = campaign.get_text_val("name", "?")
title = campaign.get_text_val("title")
type = campaign.get_text_val("type", "")
version = campaign.get_text_val("version", "")
uploads = campaign.get_text_val("uploads", "")
dependencies = campaign.get_text_val("dependencies", "")
if re.escape(args.download).replace("\\_", "_") == args.download:
if name == args.download:
fetchlist.append((name, title, version, type, uploads, dependencies))
elif not args.type or args.type == type:
if re.search(args.download, name):
fetchlist.append((name, title, version, type, uploads, dependencies))
for name, title, version, type, uploads, dependencies in fetchlist:
info = os.path.join(args.campaigns_dir, name, "_info.cfg")
local_uploads, local_version = get_info(info)
if uploads != local_uploads:
# The uploads > local_uploads likely means a server reset
if version != local_version or uploads > local_uploads:
get(name, title, version, type, uploads, dependencies, args.campaigns_dir)
else:
print("Not downloading", name, \
"as the version already is", local_version, \
"(The add-on got re-uploaded.)")
else:
if args.verbose:
print("Not downloading", name, \
"because it is already up-to-date.")
elif args.unpack:
cs = CampaignClient(address)
data = file(args.unpack).read()
decoded = cs.decode(data)
print("Unpacking %s..." % args.unpack)
cs.unpackdir(decoded, args.campaigns_dir, verbose=True)
elif args.remove:
cs = CampaignClient(address)
data = cs.delete_campaign(*args.remove)
print_messages(data)
elif args.change_passphrase:
cs = CampaignClient(address)
data = cs.change_passphrase(*args.change_passphrase)
print_messages(data)
elif args.upload:
cs = CampaignClient(address)
if os.path.isdir(args.upload):
# else basename returns an empty string
args.upload = args.upload.rstrip("/")
# New style with _server.pbl
pbl_file_name = os.path.join(args.upload, "_server.pbl")
name = os.path.basename(os.path.realpath(args.upload))
wmldir = args.upload
cfgfile = None # _main.cfg will be uploaded with the rest
ignfile = os.path.join(args.upload, "_server.ign")
else:
# Old style with external .pbl file
pbl_file_name = args.upload
name = os.path.basename(args.upload)
name = os.path.splitext(name)[0]
wmldir = os.path.join(os.path.dirname(args.upload), name)
cfgfile = args.upload.replace(".pbl", ".cfg")
ignfile = args.upload.replace(".pbl", ".ign")
if args.pbl:
pbl_file_name = args.pbl
with open(pbl_file_name, 'r') as pbl_file:
pbl_text = pbl_file.read()
for key_value_pair in args.pbl_key or []:
key = key_value_pair[0]
value = key_value_pair[1]
if not re.match("^[a-zA-Z]+[_a-zA-Z]*$", key):
raise ValueError("non-standard --pbl-key " + key)
pbl_text = pbl_text + '\n' + key + '="' + value.replace('"', '""') + '"'
pbl = parse_wml_text(pbl_text)
if os.path.exists(ignfile):
ign = open(ignfile).readlines()
# strip line endings and whitespace
ign = [i.strip() for i in ign if i.strip()]
else:
ign = [
".*",
".*/",
"#*#",
"*~",
"*-bak",
"*.swp",
"*.pbl",
"*.ign",
"_info.cfg",
"*.exe",
"*.bat",
"*.cmd",
"*.com",
"*.scr",
"*.sh",
"*.js",
"*.vbs",
"*.o",
"*.ini",
"Thumbs.db",
"*.wesnoth",
"*.project",
"__MACOSX/"]
mythread = cs.put_campaign_async(name, cfgfile, wmldir, ign, pbl)
pcounter = 0
while not mythread.event.isSet():
mythread.event.wait(1)
if cs.counter != pcounter:
print("%d/%d" % (cs.counter, cs.length))
pcounter = cs.counter
print_messages(mythread.data)
elif args.update or args.status:
if args.status:
cdir = args.status
else:
cdir = args.update
dirs = glob.glob(os.path.join(cdir, "*"))
dirs = [x for x in dirs if os.path.isdir(x)]
cs = CampaignClient(address)
campaign_list = data = cs.list_campaigns()
if not data:
sys.stderr.write("Could not connect to the add-on server.\n")
sys.exit(-1)
campaigns = {}
for c in data.get_all(tag = "campaigns")[0].get_all(tag = "campaign"):
name = c.get_text_val("name")
campaigns[name] = c
for d in dirs:
dirname = os.path.basename(d)
if dirname in campaigns:
info = os.path.join(d, "_info.cfg")
stitle = campaigns[dirname].get_text_val("title", "")
sversion = campaigns[dirname].get_text_val("version", "")
stype = campaigns[dirname].get_text_val("type", "")
srev = campaigns[dirname].get_text_val("uploads", "")
sdeps = campaigns[dirname].get_text_val("dependencies", "")
if os.path.exists(info):
lrev, lversion = get_info(info)
if not srev:
sys.stdout.write(" ? " + dirname + " - has no " +
"version info on the server.\n")
elif srev == lrev:
sys.stdout.write(" " + dirname +
" - is up to date.\n")
elif sversion == lversion:
sys.stdout.write(" # " + dirname + " - is version " +
sversion + (" but you have revision %s not %s." +
" (The add-on got re-uploaded.)\n") %
(lrev, srev))
if srev > lrev: # server reset?
if args.update:
get(dirname, stitle, sversion, stype, srev, sdeps, cdir)
else:
sys.stdout.write(" * " + dirname + " - you have " +
"revision " + lrev + " but revision " + srev +
" is available.\n")
if args.update: get(dirname, stitle, sversion, stype, srev, sdeps, cdir)
else:
sys.stdout.write(" ? " + dirname +
" - is installed but has no " +
"version info.\n")
if args.update: get(dirname, stitle, sversion, stype, srev, sdeps, cdir)
else:
sys.stdout.write(" - %s - is installed but not on server.\n" %
dirname)
elif args.html:
pass
else:
argumentparser.print_help()
if args.html:
if not campaign_list:
cs = CampaignClient(address)
campaign_list = cs.list_campaigns()
del cs
if campaign_list:
import addon_manager.html
addon_manager.html.output(args.html, args.url, args.datadir,
campaign_list)
else:
sys.stderr.write("Could not retrieve campaign list " +
"for HTML output.\n")