pyarmor.cli 9.2.5__2-py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyarmor/cli/command.py ADDED
@@ -0,0 +1,563 @@
1
+ #! /usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ #############################################################
5
+ # #
6
+ # Copyright @ 2024 Dashingsoft corp. #
7
+ # All rights reserved. #
8
+ # #
9
+ # pyarmor #
10
+ # #
11
+ # Version: 9.1.0 - #
12
+ # #
13
+ #############################################################
14
+ #
15
+ #
16
+ # @File: pyarmor/cli/commander.py
17
+ #
18
+ # @Author: Jondy Zhao(pyarmor@163.com)
19
+ #
20
+ # @Create Date: Tue Nov 12 16:38:51 CST 2024
21
+ #
22
+ import argparse
23
+ import configparser
24
+ import shlex
25
+
26
+ from os import makedirs, remove as rmfile
27
+ from os.path import abspath, exists, join as joinpath, relpath
28
+
29
+ from . import logger, CliError
30
+ from .project import Project
31
+ from .shell import PyarmorShell
32
+
33
+
34
+ class Commander:
35
+
36
+ def init_parser(self, subparsers):
37
+ parser = subparsers.add_parser(
38
+ 'init',
39
+ aliases=['i'],
40
+ formatter_class=argparse.RawDescriptionHelpFormatter,
41
+ description='init project in current path',
42
+ help='init project in current path'
43
+ )
44
+
45
+ parser.add_argument(
46
+ '-s', '--src', metavar='PATH', default='',
47
+ help='where to find scripts, modules and packages'
48
+ )
49
+ parser.add_argument(
50
+ '-e', '--entry', dest='scripts', metavar='FILE',
51
+ action='append',
52
+ help='project entry scripts'
53
+ )
54
+ parser.add_argument(
55
+ '-m', '--module', metavar='FILE',
56
+ dest='modules', action='append',
57
+ help='append extra module'
58
+ )
59
+ parser.add_argument(
60
+ '-p', '--package', metavar='PATH',
61
+ dest='packages', action='append',
62
+ help='append extra package'
63
+ )
64
+
65
+ parser.add_argument(
66
+ '-x', '--exclude', metavar='PATTERN',
67
+ dest='excludes', action='append',
68
+ help='exclude file or path'
69
+ )
70
+
71
+ parser.add_argument(
72
+ '-r', '--recursive', action='store_true',
73
+ help='Search modules and packages recursively'
74
+ )
75
+ parser.add_argument(
76
+ '-C', '--clean', action='store_true',
77
+ help='remove old project information before init'
78
+ )
79
+
80
+ parser.set_defaults(func=self.cmd_init)
81
+
82
+ def env_parser(self, subparsers):
83
+ parser = subparsers.add_parser(
84
+ 'env',
85
+ aliases=['environ', 'e'],
86
+ formatter_class=argparse.RawDescriptionHelpFormatter,
87
+ description='check and set Pyarmor environments',
88
+ help='check and set Pyarmor environments'
89
+ )
90
+
91
+ # parser.add_argument(
92
+ # '-i', '--interactive', action='store_true',
93
+ # help='enable interactive mode'
94
+ # )
95
+
96
+ group = parser.add_argument_group(
97
+ 'select domain (default: project)'
98
+ ).add_mutually_exclusive_group()
99
+ group.add_argument(
100
+ '-l', '--local', dest='domain', default='local',
101
+ action="store_const", const='local',
102
+ help='enter local domain'
103
+ )
104
+ group.add_argument(
105
+ '-g', '--global', dest='domain', default='local',
106
+ action="store_const", const='global',
107
+ help='enter global domain'
108
+ )
109
+ group.add_argument(
110
+ '-p', '--project', dest='domain', default='local',
111
+ action="store_const", const='project',
112
+ help='enter project domain'
113
+ )
114
+
115
+ parser.add_argument(
116
+ 'exprs', metavar='VERB', nargs='*',
117
+ help='change option by info/ls/set/reset/push/pop'
118
+ )
119
+
120
+ parser.set_defaults(func=self.cmd_env)
121
+
122
+ def build_parser(self, subparsers):
123
+ parser = subparsers.add_parser(
124
+ 'build',
125
+ aliases=['b'],
126
+ formatter_class=argparse.RawTextHelpFormatter,
127
+ help='obfuscate all the scripts in the project'
128
+ )
129
+
130
+ group = parser.add_argument_group(
131
+ 'select build target'
132
+ ).add_mutually_exclusive_group()
133
+ # group.add_argument(
134
+ # '--std', dest='target', default='std',
135
+ # action="store_const", const='std',
136
+ # help='genetate standard obfuscated scripts'
137
+ # )
138
+ group.add_argument(
139
+ '--rft', dest='target', default='std',
140
+ action="store_const", const='rft',
141
+ help='only refactor scripts'
142
+ )
143
+ group.add_argument(
144
+ '--mini', dest='target', default='std',
145
+ action="store_const", const='mini',
146
+ help='genetate high performance scripts'
147
+ )
148
+ group.add_argument(
149
+ '--mini-rft', dest='target', default='std',
150
+ action="store_const", const='mini-rft',
151
+ help='generate scripts by combining --rft and --mini'
152
+ )
153
+ group.add_argument(
154
+ '--vmc', dest='target', default='std',
155
+ action="store_const", const='vmc',
156
+ help='genetate scripts with vmc code'
157
+ )
158
+ group.add_argument(
159
+ '--vmc-rft', dest='target', default='std',
160
+ action="store_const", const='vmc-rft',
161
+ help='generate scripts by combining --rft and --vmc'
162
+ )
163
+ group.add_argument(
164
+ '--ecc', dest='target', default='std',
165
+ action="store_const", const='ecc',
166
+ help='genetate scripts with embedded C code'
167
+ )
168
+ group.add_argument(
169
+ '--ecc-rft', dest='target', default='std',
170
+ action="store_const", const='ecc-rft',
171
+ help='generate scripts by combining --rft and --ecc'
172
+ )
173
+ group.add_argument(
174
+ '--ecc-nogil', dest='target', default='std',
175
+ action="store_const", const='ecc-nogil',
176
+ help='genetate ECC scripts for free-threading Python'
177
+ )
178
+ group.add_argument(
179
+ '--ecc-rft-nogil', dest='target', default='std',
180
+ action="store_const", const='ecc-rft-nogil',
181
+ help=('generate scripts by combining --rft and --ecc'
182
+ ' for free-threading Python')
183
+ )
184
+ group.add_argument(
185
+ '--list', dest='target', default='std',
186
+ action="store_const", const='list',
187
+ help='list project scripts, modules and packages'
188
+ )
189
+ group.add_argument(
190
+ '--types', dest='target', default='std',
191
+ action="store_const", const='types',
192
+ help=argparse.SUPPRESS
193
+ )
194
+ group.add_argument(
195
+ '--randname', type=int, metavar='{0,1}',
196
+ help='Build random name pool'
197
+ )
198
+ group.add_argument(
199
+ '--autofix', type=int, choices=(0, 1, 2, 3),
200
+ help='Generate refactor rules by autofix mode'
201
+ )
202
+
203
+ # parser.add_argument(
204
+ # '--pack', metavar='MODE',
205
+ # help='specify pack mode, onefile or onedir'
206
+ # )
207
+
208
+ # sgroup = parser.add_argument_group(
209
+ # title='these options only for --std target',
210
+ # )
211
+ # sgroup.add_argument(
212
+ # '--restrict',
213
+ # default=1, choices=(0, 1, 2),
214
+ # help='set restrict mode (default: 1)'
215
+ # )
216
+ # sgroup.add_argument(
217
+ # '--assert-import', action="store_true",
218
+ # help='check imported module is obfuscated'
219
+ # )
220
+ # sgroup.add_argument(
221
+ # '--assert-call', action="store_true",
222
+ # help='check function is obfuscated before call'
223
+ # )
224
+
225
+ parser.add_argument(
226
+ '-O', '--output', metavar='PATH',
227
+ help='output path, default is "dist"'
228
+ )
229
+
230
+ # parser.add_argument(
231
+ # 'project', metavar='PATH', nargs='?',
232
+ # help='project path (default: current path)'
233
+ # )
234
+
235
+ parser.set_defaults(func=self.cmd_build)
236
+
237
+ def main_parser(self):
238
+ parser = argparse.ArgumentParser(
239
+ prog='pyarmor',
240
+ fromfile_prefix_chars='@',
241
+ formatter_class=argparse.RawDescriptionHelpFormatter,
242
+ )
243
+ parser.add_argument(
244
+ '-v', '--version', action='store_true',
245
+ help='show version information and exit'
246
+ )
247
+ parser.add_argument(
248
+ '-q', '--silent', action='store_true',
249
+ help='suppress all normal output'
250
+ )
251
+
252
+ subparsers = parser.add_subparsers(
253
+ title='The most commonly used pyarmor commands are',
254
+ )
255
+
256
+ self.env_parser(subparsers)
257
+ self.init_parser(subparsers)
258
+ self.build_parser(subparsers)
259
+
260
+ return parser
261
+
262
+ def run(self, ctx, argv):
263
+ parser = self.main_parser()
264
+ args = parser.parse_args(argv)
265
+ if hasattr(args, 'func'):
266
+ args.func(ctx, args)
267
+ else:
268
+ parser.print_help()
269
+
270
+ def cmd_init(self, ctx, args):
271
+ logger.debug('init %s', args)
272
+
273
+ sep = ','
274
+ cfgsep = ' '
275
+ data = {}
276
+
277
+ cfg = configparser.ConfigParser(
278
+ empty_lines_in_values=False,
279
+ interpolation=configparser.ExtendedInterpolation(),
280
+ )
281
+ cfg.read([ctx.local_config], encoding=ctx.encoding)
282
+
283
+ sectname = 'project'
284
+ if args.clean:
285
+ logger.info('clean old project')
286
+ if cfg.has_section(sectname):
287
+ logger.debug('remove cfg section: project')
288
+ cfg.remove_section(sectname)
289
+ if cfg.has_section('rft'):
290
+ logger.debug('remove cfg section: rft')
291
+ cfg.remove_section('rft')
292
+ propath = joinpath(ctx.local_path, 'project')
293
+ for name in ['rft_autofix.rules']:
294
+ profile = joinpath(propath, name)
295
+ if exists(profile):
296
+ logger.debug('remove file: %s', profile)
297
+ rmfile(profile)
298
+
299
+ if cfg.has_section(sectname):
300
+ logger.info('change project settings')
301
+ else:
302
+ cfg.add_section(sectname)
303
+
304
+ sect = cfg[sectname]
305
+ if args.src:
306
+ src = abspath(args.src)
307
+ if not sect.get('src'):
308
+ sect['src'] = src
309
+ elif src != sect['src']:
310
+ raise CliError(
311
+ 'project has another src, '
312
+ 'use option "-C" to fix this issue'
313
+ )
314
+ else:
315
+ src = sect.get('src')
316
+ if not src:
317
+ src = abspath('.')
318
+ sect['src'] = src
319
+
320
+ if not exists(src):
321
+ raise CliError('no found src "%s"' % src)
322
+
323
+ def format_path(plist, raw=False):
324
+ r = []
325
+ for x in sep.join(plist).split(sep):
326
+ x = x.strip()
327
+ r.append(x if raw else relpath(abspath(x), src))
328
+ return r
329
+
330
+ if args.scripts:
331
+ data['scripts'] = format_path(args.scripts)
332
+
333
+ if args.excludes:
334
+ data['excludes'] = format_path(args.excludes, raw=1)
335
+
336
+ if args.modules:
337
+ data['modules'] = format_path(args.modules)
338
+
339
+ if args.packages:
340
+ data['packages'] = format_path(args.packages)
341
+
342
+ if args.recursive:
343
+ data['recursive'] = '1'
344
+
345
+ for key, value in data.items():
346
+ old = sect.get(key)
347
+ if old is not None:
348
+ logger.info('overwrite %s: %s', key, old)
349
+ if isinstance(value, (tuple, list)):
350
+ value = cfgsep.join([
351
+ x.replace(' ', '%20%') for x in value
352
+ ])
353
+ sect[key] = value
354
+
355
+ logger.info('project information')
356
+ for key, value in cfg.items(sectname):
357
+ if key in ('src',):
358
+ value = relpath(value)
359
+ logger.info('%-20s: %s', key, value)
360
+
361
+ makedirs(ctx.local_path, exist_ok=True)
362
+ with open(ctx.local_config, 'w', encoding=ctx.encoding) as f:
363
+ cfg.write(f)
364
+ logger.info('project saved')
365
+
366
+ def cmd_build(self, ctx, args):
367
+ """Obfuscate all scripts, modules and packages in project
368
+
369
+ Show project information:
370
+
371
+ pyarmor i
372
+
373
+ Generate plain scripts which no extra extension, the final
374
+ scripts could be taken as input for Nuitka, Cython etc.:
375
+
376
+ pyarmor build --rft
377
+
378
+ Generate high performance scripts:
379
+
380
+ pyarmor build --mini
381
+
382
+ """
383
+ logger.debug('build %s', args)
384
+ cfg = configparser.ConfigParser(
385
+ empty_lines_in_values=False,
386
+ interpolation=configparser.ExtendedInterpolation(),
387
+ )
388
+ cfg.read([ctx.local_config], encoding=ctx.encoding)
389
+
390
+ x, y = ctx.python_version
391
+ if not (x == 3 and y > 8 and y < 16):
392
+ raise CliError('`pyarmor build` only works for Python 3.9+')
393
+
394
+ sectname = 'project'
395
+ if not cfg.has_section(sectname):
396
+ raise CliError('no project information')
397
+
398
+ cfgdata = dict(cfg.items(sectname))
399
+ logger.debug('project: %s', cfgdata)
400
+
401
+ project = Project(ctx)
402
+ logger.info('load project ...')
403
+ project.load(cfgdata)
404
+ logger.info('load project end')
405
+
406
+ if args.target == 'list':
407
+ logger.info('project src: %s', relpath(project.src))
408
+ logger.info('project scripts:')
409
+ for x in project.scripts:
410
+ logger.info(' %s', project.relsrc(x.abspath))
411
+ logger.info('project modules:')
412
+ for x in project.modules:
413
+ logger.info(' %s', project.relsrc(x.abspath))
414
+ logger.info('project packages:')
415
+ for x in project.packages:
416
+ logger.info(' %s', x.name)
417
+ mlist = [m.abspath for m in project.iter_module()]
418
+ mlist.sort()
419
+ for s in mlist:
420
+ logger.info(' %s', project.relsrc(s))
421
+ return
422
+
423
+ if args.autofix is not None:
424
+ value = args.autofix
425
+ logger.info('build auto-fix-table:%s ...', value)
426
+ output = args.output if args.output else 'dist'
427
+ self._build(project, 'autofix', output, value)
428
+ logger.info('build auto-fix-table:%s end', value)
429
+ project.preview_autofix_result(value)
430
+ elif args.randname is not None:
431
+ value = args.randname
432
+ logger.info('build rand-pool:%s ...', value)
433
+ output = args.output if args.output else 'dist'
434
+ self._build(project, 'namepool', output, value)
435
+ logger.info('build rand-pool:%s end', value)
436
+ else:
437
+ logger.info('build target %s ...', args.target)
438
+ output = args.output if args.output else 'dist'
439
+ self._build(project, args.target, output)
440
+ if args.target.startswith('std'):
441
+ self._cmd_gen(args)
442
+ logger.info('build target %s end', args.target)
443
+
444
+ def _build(self, project, target, output, value=None):
445
+ from pyarmor.cli.core import Pytransform3
446
+ args = [self.ctx, target, project, output, value]
447
+ m = Pytransform3.init(self.ctx)
448
+ m.pre_build(args)
449
+
450
+ def _cmd_gen(self, args):
451
+ """If build target is std, then call `pyarmor gen`
452
+
453
+ It will obfuscate the scripts with `pyarmor_runtime.so`
454
+
455
+ Most of obfuscated options are got from config file, not from
456
+ command line
457
+
458
+ Option `--pack` may be still got from command line
459
+ """
460
+ from .generate import Builder
461
+ from .plugin import Plugin
462
+
463
+ ctx = self.ctx
464
+ builder = Builder(ctx)
465
+ Plugin.install(ctx)
466
+ builder.process(args)
467
+ Plugin.post_build(ctx)
468
+
469
+ def cmd_env(self, ctx, args):
470
+ """Check and change pyarmor settings
471
+
472
+ Enter interactive mode:
473
+
474
+ pyarmor env
475
+
476
+ Show project src
477
+
478
+ pyarmor env -p get src
479
+
480
+ Change project src
481
+
482
+ pyarmor env -p set src ../src
483
+
484
+ Append project excludes
485
+
486
+ pyarmor env -p push excludes "test*"
487
+
488
+ Remove project excludes
489
+
490
+ pyarmor env -p pop excludes "test*"
491
+
492
+ Clear project excludes
493
+
494
+ pyarmor env -p reset excludes
495
+
496
+ """
497
+ logger.debug('env %s', args)
498
+ logger.info('enter domain: %s', args.domain)
499
+ shell = PyarmorShell(ctx, domain=args.domain)
500
+
501
+ if args.exprs:
502
+ exprs = args.exprs
503
+ n = len(exprs)
504
+ if n < 2:
505
+ logger.error('missing parameters')
506
+ return
507
+
508
+ verb = exprs[0]
509
+ opt = exprs[1]
510
+
511
+ i = opt.find(':')
512
+ if i > 0:
513
+ section, opt = opt[:i], opt[i+1:]
514
+ shell.do_cd(section)
515
+
516
+ if verb == 'reset':
517
+ shell.do_reset(opt)
518
+ elif verb == 'get':
519
+ shell.do_get(opt)
520
+ elif verb == 'info':
521
+ shell.do_info(opt)
522
+ elif n < 3:
523
+ logger.error('missing parameters')
524
+ return
525
+ else:
526
+ arg = shlex.join([opt] + exprs[2:])
527
+ if verb == 'pop':
528
+ shell.do_pop(arg)
529
+ elif verb == 'push':
530
+ shell.do_push(arg)
531
+ elif verb == 'set':
532
+ shell.do_set(arg)
533
+ else:
534
+ logger.error('unknown verb "%s"', verb)
535
+ return
536
+
537
+ shell.save()
538
+
539
+ else:
540
+ shell.cmdloop()
541
+
542
+
543
+ def test_main(args, target='rft', log=False):
544
+ import logging.config
545
+ from os.path import expanduser
546
+
547
+ from .context import Context
548
+ from .rftbuild import rft_build_project
549
+
550
+ home = joinpath('~', '.pyarmor')
551
+ home = abspath(expanduser(home))
552
+
553
+ ctx = Context(home)
554
+ if log:
555
+ logging.config.fileConfig(ctx.default_config)
556
+ cmd = Commander()
557
+ cmd._build = rft_build_project
558
+ cmd.run(ctx, args)
559
+
560
+
561
+ if __name__ == '__main__':
562
+ import sys
563
+ test_main(sys.argv[1:], log=True)