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/repack.py ADDED
@@ -0,0 +1,793 @@
1
+ import logging
2
+ import marshal
3
+ import os
4
+ import shutil
5
+ import sys
6
+
7
+ from fnmatch import fnmatch
8
+ from subprocess import check_call, DEVNULL
9
+
10
+ # Only used by BundleRepacker, they could be removed with deprecated routines
11
+ import struct
12
+ import tempfile
13
+ from importlib._bootstrap_external import _code_to_timestamp_pyc
14
+ from PyInstaller.archive.writers import ZlibArchiveWriter, CArchiveWriter
15
+ from PyInstaller.archive.readers import CArchiveReader
16
+ from PyInstaller.compat import is_darwin, is_linux, is_win
17
+
18
+
19
+ logger = logging.getLogger('repack')
20
+
21
+
22
+ def find_packer(mode):
23
+ if mode in ('onefile', 'onedir', 'F', 'D', 'FC', 'DC'):
24
+ return AutoRepacker
25
+
26
+ if isinstance(mode, str) and mode.endswith('.spec'):
27
+ return SpecRepacker
28
+
29
+ return BundleRepacker
30
+
31
+
32
+ def autoclean_output(output, autoclean=True):
33
+ if os.path.exists(output) and autoclean:
34
+ n = len(os.path.abspath(output).split(os.sep))
35
+ if n < 3:
36
+ prompt = 'Are you sure to remove path "%s" (y/n)? '
37
+ choice = input(prompt % output).lower()[:1]
38
+ if not choice == 'y':
39
+ return
40
+ logger.info('clean output path "%s"', output)
41
+ shutil.rmtree(output)
42
+
43
+
44
+ # This patch is used to modify the specfile generated by PyInstaller
45
+ #
46
+ # It mainly does:
47
+ #
48
+ # 1. Find all the imported modules and packages, save to hook script
49
+ #
50
+ # 2. Find imported modules and packages in the same path of main
51
+ # script, and save their names to one list (all of them will be
52
+ # obfuscated automatically)
53
+ #
54
+ spec_patch_code = '''
55
+ import marshal
56
+ import os
57
+
58
+ from PyInstaller.compat import base_prefix, EXTENSION_SUFFIXES
59
+ from sys import prefix, exec_prefix
60
+ from fnmatch import fnmatch
61
+
62
+ exlist = set([base_prefix, prefix, exec_prefix])
63
+ expats = [os.path.join(os.path.normpath(x), '*') for x in exlist]
64
+
65
+ src = {src!r}
66
+ resfile = {resfile!r}
67
+ hookscript = {hookscript!r}
68
+
69
+ nsrc = len(src) + 1
70
+ rsrc = os.path.relpath(src)
71
+ rsrc = '' if rsrc == '.' else rsrc
72
+ psrc = os.path.join(src, '*')
73
+
74
+ hiddenimports = set([])
75
+ plist = set([])
76
+ mlist = []
77
+
78
+ for name, path, kind in a.pure:
79
+ if name.startswith('pyi_rth'):
80
+ continue
81
+ hiddenimports.add(name)
82
+ if fnmatch(path, psrc) and not any([fnmatch(path, x) for x in expats]):
83
+ if name.find('.') == -1 and os.path.basename(path) != '__init__.py':
84
+ mlist.append(os.path.join(rsrc, path[nsrc:]))
85
+ else:
86
+ pkgname = os.path.dirname(path[nsrc:]).split(os.sep)[0]
87
+ plist.add(os.path.join(rsrc, pkgname))
88
+
89
+ for name, path, kind in a.binaries:
90
+ if kind == 'EXTENSION':
91
+ for x in EXTENSION_SUFFIXES:
92
+ if fnmatch(name, '*' + x):
93
+ name = name[:-len(x)]
94
+ break
95
+ hiddenimports.add(name.replace(os.sep, '.'))
96
+
97
+ with open(resfile, 'wb') as f:
98
+ marshal.dump(mlist + list(plist), f)
99
+ with open(hookscript, 'w', encoding='utf-8') as f:
100
+ f.write("hiddenimports=[%s]" % ", ".join([repr(x) for x in hiddenimports]))
101
+ '''
102
+
103
+
104
+ class AutoRepacker:
105
+ """New repacker to support PyInstaller 6.0+
106
+
107
+ Args:
108
+ ctx: build context
109
+ mode: onefile or onedir
110
+ inputs: main script to pack
111
+ output: path to store final bundle, default is `dist`
112
+ """
113
+
114
+ def __init__(self, ctx, mode, inputs, output):
115
+ self.ctx = ctx
116
+ self.script = inputs[0]
117
+ self.output = os.path.normpath(output) if output else 'dist'
118
+
119
+ self.name = os.path.splitext(os.path.basename(self.script))[0]
120
+ self.obfpath = os.path.normpath(self.ctx.pack_obfpath)
121
+ self.packpath = os.path.normpath(self.ctx.pack_basepath)
122
+ self.workpath = os.path.join(self.packpath, 'build')
123
+ self.pyicmd = [sys.executable, '-m', 'PyInstaller']
124
+ self.autoclean = mode in ('FC', 'DC')
125
+ self.modeopt = '-F' if mode in ('onefile', 'F', 'FC') else '-D'
126
+ self.init_opts()
127
+
128
+ def init_opts(self):
129
+ self.pyiopts = []
130
+
131
+ opts = self.ctx.pyi_options
132
+ exopts = '--noconfirm', '-y', '--onefile', '-F', '--onedir', '-D'
133
+ exvalues = '--distpath', '--specpath', '--workpath'
134
+
135
+ n = 0
136
+ while n < len(opts):
137
+ x = opts[n]
138
+ if x in ('--name', '-n'):
139
+ self.name = opts[n+1]
140
+
141
+ if x in ('--noconfirm', '-y'):
142
+ self.autoclean = True
143
+
144
+ if x in exvalues:
145
+ n += 1
146
+ elif x not in exopts:
147
+ self.pyiopts.append(x)
148
+ n += 1
149
+
150
+ def analysis(self):
151
+ """Got imported modules and packages by PyInstaller
152
+
153
+ Generate hook script
154
+ Return file/dir list need to be obfuscated
155
+ """
156
+ cmdspec = [sys.executable, '-m', 'PyInstaller.utils.cliutils.makespec']
157
+ cmdspec.extend(self.pyiopts)
158
+ cmdspec.append(self.script)
159
+ logger.debug('%s', ' '.join(cmdspec))
160
+ logger.info('call PyInstaller to generate specfile')
161
+ check_call(cmdspec, stdout=DEVNULL, stderr=DEVNULL)
162
+
163
+ specfile = self.name + '.spec'
164
+ rtname = self.ctx.runtime_package_name
165
+ resfile = os.path.join(self.packpath, 'resources.list')
166
+ hookscript = os.path.join(self.packpath, 'hook-%s.py' % rtname)
167
+ self.patch_specfile(specfile, hookscript, resfile)
168
+
169
+ cmdlist = self.pyicmd + ['--clean', '--workpath', self.workpath]
170
+ cmdlist.append(specfile)
171
+ logger.debug('%s', ' '.join(cmdlist))
172
+ logger.info('call PyInstaller to analysis, '
173
+ 'it may take several minutes ...')
174
+ check_call(cmdlist, stdout=DEVNULL, stderr=DEVNULL)
175
+
176
+ with open(resfile, 'rb') as f:
177
+ reslist = marshal.load(f)
178
+
179
+ resoptions = self.ctx.get_res_options('')
180
+ excludes = resoptions.get('excludes', '').split()
181
+ if excludes:
182
+ reslist = [x for x in reslist
183
+ if not any([fnmatch(x, pat) for pat in excludes])]
184
+ return reslist
185
+
186
+ def build(self):
187
+ """Generate final bundle to output"""
188
+ obfscript = os.path.join(self.obfpath, os.path.basename(self.script))
189
+ cmdlist = self.pyicmd + [
190
+ '--clean',
191
+ '--distpath', self.output,
192
+ '--workpath', self.workpath,
193
+ '--additional-hooks-dir', self.packpath,
194
+ self.modeopt
195
+ ]
196
+ cmdlist.extend(self.pyiopts)
197
+ cmdlist.append(obfscript)
198
+
199
+ logger.debug('%s', ' '.join(cmdlist))
200
+ logger.info('call PyInstaller to generate final bundle ...\n')
201
+ check_call(cmdlist)
202
+ logger.info('')
203
+ logger.info('the final bundle has been generated to "%s" successfully',
204
+ self.output)
205
+
206
+ def patch_specfile(self, specfile, hookscript, resfile):
207
+ lines = []
208
+ with open(specfile, 'r', encoding='utf-8') as f:
209
+ for line in f:
210
+ if line.startswith('pyz = PYZ'):
211
+ break
212
+ lines.append(line)
213
+ else:
214
+ logger.error('no found line starts with "pyz = PYZ"')
215
+ raise RuntimeError('unsupported specfile "%s"' % specfile)
216
+
217
+ lines.append(spec_patch_code.format(
218
+ src=os.path.abspath(os.path.dirname(self.script)),
219
+ hookscript=os.path.abspath(hookscript),
220
+ resfile=os.path.abspath(resfile)))
221
+
222
+ with open(specfile, 'w', encoding='utf-8') as f:
223
+ f.write(''.join(lines))
224
+
225
+ def check(self):
226
+ autoclean_output(self.output, self.autoclean)
227
+
228
+
229
+ manual_spec_patch = '''
230
+ # Pyarmor patch start:
231
+
232
+ def apply_pyarmor_patch():
233
+
234
+ srcpath = {srcpath!r}
235
+ obfpath = {obfpath!r}
236
+ pkgname = {rtname!r}
237
+ pkgpath = os.path.join(obfpath, pkgname)
238
+ extpath = os.path.join(pkgname, {extname!r})
239
+
240
+ if hasattr(a.pure, '_code_cache'):
241
+ code_cache = a.pure._code_cache
242
+ else:
243
+ from PyInstaller.config import CONF
244
+ code_cache = CONF['code_cache'].get(id(a.pure))
245
+
246
+ srclist = [os.path.normcase(x) for x in srcpath]
247
+ def match_obfuscated_script(orgpath):
248
+ for x in srclist:
249
+ if os.path.normcase(orgpath).startswith(x):
250
+ return os.path.join(obfpath, orgpath[len(x)+1:])
251
+
252
+ count = 0
253
+ for i in range(len(a.scripts)):
254
+ x = match_obfuscated_script(a.scripts[i][1])
255
+ if x and os.path.exists(x):
256
+ a.scripts[i] = a.scripts[i][0], x, a.scripts[i][2]
257
+ count += 1
258
+ if count == 0:
259
+ raise RuntimeError('No obfuscated script found')
260
+
261
+ for i in range(len(a.pure)):
262
+ x = match_obfuscated_script(a.pure[i][1])
263
+ if x and os.path.exists(x):
264
+ code_cache.pop(a.pure[i][0], None)
265
+ a.pure[i] = a.pure[i][0], x, a.pure[i][2]
266
+
267
+ a.pure.append((pkgname, os.path.join(pkgpath, '__init__.py'), 'PYMODULE'))
268
+ a.binaries.append((extpath, os.path.join(obfpath, extpath), 'EXTENSION'))
269
+
270
+ apply_pyarmor_patch()
271
+
272
+ # Pyarmor patch end.
273
+ '''
274
+
275
+
276
+ class SpecRepacker:
277
+ """Patch specfile and use it to pack obfuscated scripts
278
+
279
+ Args:
280
+ ctx: build context
281
+ specfile: specfile need to be patched
282
+ inputs: main script to pack
283
+ output: path to store final bundle, default is `dist`
284
+ """
285
+
286
+ def __init__(self, ctx, specfile, inputs, output):
287
+ self.ctx = ctx
288
+ self.specfile = specfile
289
+ self.script = inputs[0]
290
+ self.output = os.path.normpath(output) if output else 'dist'
291
+
292
+ self.pyicmd = [sys.executable, '-m', 'PyInstaller']
293
+ self.obfpath = os.path.normpath(self.ctx.pack_obfpath)
294
+
295
+ def build(self):
296
+ """Generate patched specfile and pack obfuscated scripts"""
297
+ specfile = self.specfile[:-5] + '.patched.spec'
298
+ logger.info('generate patched specfile "%s"', specfile)
299
+
300
+ rtpkg = self.ctx.runtime_package_name
301
+ for x in os.listdir(os.path.join(self.obfpath, rtpkg)):
302
+ if x.startswith('pyarmor_runtime'):
303
+ extname = x
304
+ break
305
+ else:
306
+ raise RuntimeError('no found extension `pyarmor_runtime`')
307
+
308
+ patch = manual_spec_patch.format(
309
+ srcpath=[os.path.abspath(os.path.dirname(self.script))],
310
+ obfpath=os.path.abspath(self.obfpath),
311
+ rtname=self.ctx.runtime_package_name,
312
+ extname=extname)
313
+
314
+ with open(self.specfile, 'r', encoding='utf-8') as f:
315
+ lines = f.readlines()
316
+
317
+ n = 0
318
+ for line in lines:
319
+ if line.startswith('pyz = PYZ'):
320
+ lines[n:n] = [patch]
321
+ break
322
+ n += 1
323
+ else:
324
+ logger.error('no found line starts with "pyz = PYZ"')
325
+ raise RuntimeError('unsupported specfile "%s"' % self.specfile)
326
+
327
+ with open(specfile, 'w', encoding='utf-8') as f:
328
+ f.write(''.join(lines))
329
+
330
+ cmdlist = self.pyicmd + [
331
+ '--clean',
332
+ '--distpath', self.output,
333
+ '--workpath', os.path.join(self.ctx.pack_basepath, 'build'),
334
+ specfile
335
+ ]
336
+ logger.info('call PyInstaller to generate final bundle ...'
337
+ '\n\n%s\n', ' '.join(cmdlist))
338
+ check_call(cmdlist)
339
+ logger.info('')
340
+ logger.info('the final bundle has been generated to "%s" successfully',
341
+ self.output)
342
+
343
+ def check(self):
344
+ options = self.ctx.pyi_options
345
+ autoclean = '-y' in options or '--noconfirm' in options
346
+ autoclean_output(self.output, autoclean)
347
+
348
+
349
+ ##############################################################
350
+ #
351
+ # Deprecated routines
352
+ #
353
+ # This is the old method to repack executable generated by PyInstaller
354
+ #
355
+ # Why is it deprecated?
356
+ #
357
+ # 1. It need hack PyInstaller bundle structure which may be changed in
358
+ # different PyInstaller version.
359
+ #
360
+ # Actually, it doesn't work for PyInstaller 5.13+ because of this
361
+ #
362
+ # 2. Not friend to junior users
363
+ #
364
+ ##
365
+
366
+ # Type codes for PYZ PYZ entries
367
+ PYZ_ITEM_MODULE = 0
368
+ PYZ_ITEM_PKG = 1
369
+ PYZ_ITEM_DATA = 2
370
+ PYZ_ITEM_NSPKG = 3 # PEP-420 namespace package
371
+
372
+ # Type codes for CArchive TOC entries
373
+ PKG_ITEM_BINARY = 'b' # binary
374
+ PKG_ITEM_DEPENDENCY = 'd' # runtime option
375
+ PKG_ITEM_PYZ = 'z' # zlib (pyz) - frozen Python code
376
+ PKG_ITEM_ZIPFILE = 'Z' # zlib (pyz) - frozen Python code
377
+ PKG_ITEM_PYPACKAGE = 'M' # Python package (__init__.py)
378
+ PKG_ITEM_PYMODULE = 'm' # Python module
379
+ PKG_ITEM_PYSOURCE = 's' # Python script (v3)
380
+ PKG_ITEM_DATA = 'x' # data
381
+ PKG_ITEM_RUNTIME_OPTION = 'o' # runtime option
382
+ PKG_ITEM_SPLASH = 'l' # splash resources
383
+
384
+ # Path suffix for extracted contents
385
+ EXTRACT_SUFFIX = '_extracted'
386
+
387
+
388
+ class CArchiveReader2(CArchiveReader):
389
+
390
+ def find_magic_pattern(self, fp, magic_pattern):
391
+ # Start at the end of file, and scan back-to-start
392
+ fp.seek(0, os.SEEK_END)
393
+ end_pos = fp.tell()
394
+
395
+ # Scan from back
396
+ SEARCH_CHUNK_SIZE = 8192
397
+ magic_offset = -1
398
+ while end_pos >= len(magic_pattern):
399
+ start_pos = max(end_pos - SEARCH_CHUNK_SIZE, 0)
400
+ chunk_size = end_pos - start_pos
401
+ # Is the remaining chunk large enough to hold the pattern?
402
+ if chunk_size < len(magic_pattern):
403
+ break
404
+ # Read and scan the chunk
405
+ fp.seek(start_pos, os.SEEK_SET)
406
+ buf = fp.read(chunk_size)
407
+ pos = buf.rfind(magic_pattern)
408
+ if pos != -1:
409
+ magic_offset = start_pos + pos
410
+ break
411
+ # Adjust search location for next chunk; ensure proper overlap
412
+ end_pos = start_pos + len(magic_pattern) - 1
413
+
414
+ return magic_offset
415
+
416
+ def get_cookie_info(self, fp):
417
+ magic = getattr(self, '_COOKIE_MAGIC_PATTERN',
418
+ getattr(self, 'MAGIC', b'MEI\014\013\012\013\016'))
419
+ cookie_pos = self.find_magic_pattern(fp, magic)
420
+
421
+ cookie_format = getattr(self, '_COOKIE_FORMAT',
422
+ getattr(self, '_cookie_format', '!8sIIii64s'))
423
+ cookie_size = struct.calcsize(cookie_format)
424
+
425
+ fp.seek(cookie_pos, os.SEEK_SET)
426
+ return struct.unpack(cookie_format, fp.read(cookie_size))
427
+
428
+ def get_toc(self):
429
+ if isinstance(self.toc, dict):
430
+ return self.toc
431
+ return {entry[-1]: entry[:-1] for entry in self.toc}
432
+
433
+ def open_pyzarchive(self, name):
434
+ if hasattr(self, 'open_embedded_archive'):
435
+ return self.open_embedded_archive(name)
436
+
437
+ try:
438
+ from PyInstaller.loader.pyimod02_archive import ZlibArchiveReader
439
+ except ModuleNotFoundError:
440
+ # Since 5.3
441
+ from PyInstaller.loader.pyimod01_archive import ZlibArchiveReader
442
+
443
+ ndx = self.toc.find(name)
444
+ (dpos, dlen, ulen, flag, typcd, nm) = self.toc.get(ndx)
445
+ return ZlibArchiveReader(self.path, self.pkg_start + dpos)
446
+
447
+ def get_logical_toc(self, buildpath, obfpath):
448
+ logical_toc = []
449
+
450
+ for name, entry in self.get_toc().items():
451
+ *_, flag, typecode = entry
452
+ if typecode == PKG_ITEM_PYMODULE:
453
+ source = os.path.join(obfpath, name + '.py')
454
+ elif typecode == PKG_ITEM_PYSOURCE:
455
+ source = os.path.join(obfpath, name + '.py')
456
+ elif typecode == PKG_ITEM_PYPACKAGE:
457
+ source = os.path.join(obfpath, name, '__init__.py')
458
+ elif typecode == PKG_ITEM_PYZ:
459
+ source = os.path.join(buildpath, name)
460
+ elif typecode in (PKG_ITEM_DEPENDENCY, PKG_ITEM_RUNTIME_OPTION):
461
+ source = ''
462
+ else:
463
+ source = None
464
+ if source and not os.path.exists(source):
465
+ source = None
466
+ logical_toc.append((name, source, flag, typecode))
467
+
468
+ return logical_toc
469
+
470
+
471
+ class CArchiveWriter2(CArchiveWriter):
472
+
473
+ def __init__(self, pkg_arch, archive_path, logical_toc, pylib_name):
474
+ self._orgarch = pkg_arch
475
+ super().__init__(archive_path, logical_toc, pylib_name)
476
+
477
+ def _write_rawdata(self, name, typecode, compress):
478
+ rawdata = fix_extract(self._orgarch.extract(name))
479
+ if hasattr(self, '_write_blob'):
480
+ # Since 5.0
481
+ self._write_blob(rawdata, name, typecode, compress)
482
+ return
483
+
484
+ with tempfile.TemporaryDirectory() as tmpdir:
485
+ pathname = os.path.join(tmpdir,
486
+ name.replace('/', '_').replace('\\', '_'))
487
+ with open(pathname, 'wb') as f:
488
+ f.write(rawdata)
489
+ if typecode in (PKG_ITEM_PYSOURCE, PKG_ITEM_PYMODULE,
490
+ PKG_ITEM_PYPACKAGE):
491
+ super().add((name, pathname, compress, PKG_ITEM_DATA))
492
+ tc = self.toc.data[-1]
493
+ self.toc.data[-1] = tc[:-2] + (typecode, tc[-1])
494
+ else:
495
+ super().add((name, pathname, compress, typecode))
496
+
497
+ def add(self, entry):
498
+ name, source, compress, typecode = entry[:4]
499
+ if source is None:
500
+ self._write_rawdata(name, typecode, compress)
501
+ else:
502
+ logger.info('replace entry "%s"', name)
503
+ super().add(entry)
504
+
505
+ def _write_entry(self, fp, entry):
506
+ '''For PyInstaller 5.10+'''
507
+ name, source, compress, typecode = entry[:4]
508
+ if source is None:
509
+ rawdata = self._orgarch.extract(name)
510
+ return self._write_blob(fp, rawdata, name, typecode, compress)
511
+ return super()._write_entry(fp, entry)
512
+
513
+
514
+ def fix_extract(data):
515
+ return data[1] if isinstance(data, tuple) else data
516
+
517
+
518
+ def extract_pyzarchive(name, pyzarch, output):
519
+ dirname = os.path.join(output, name + EXTRACT_SUFFIX)
520
+ os.makedirs(dirname, exist_ok=True)
521
+
522
+ for name, (typecode, offset, length) in pyzarch.toc.items():
523
+ # Prevent writing outside dirName
524
+ filename = name.replace('..', '__').replace('.', os.path.sep)
525
+ if typecode == PYZ_ITEM_PKG:
526
+ filepath = os.path.join(dirname, filename, '__init__.pyc')
527
+ elif typecode == PYZ_ITEM_MODULE:
528
+ filepath = os.path.join(dirname, filename + '.pyc')
529
+ elif typecode == PYZ_ITEM_DATA:
530
+ filepath = os.path.join(dirname, filename)
531
+ elif typecode == PYZ_ITEM_NSPKG:
532
+ filepath = os.path.join(dirname, filename, '__init__.pyc')
533
+ else:
534
+ continue
535
+ os.makedirs(os.path.dirname(filepath), exist_ok=True)
536
+ with open(filepath, 'wb') as f:
537
+ f.write(_code_to_timestamp_pyc(fix_extract(pyzarch.extract(name))))
538
+
539
+ return dirname
540
+
541
+
542
+ def repack_pyzarchive(pyzpath, pyztoc, obfpath, rtname, cipher=None):
543
+ # logic_toc tuples: (name, src_path, typecode)
544
+ # `name` is the name without suffix)
545
+ # `src_path` is name of the file from which the resource is read
546
+ # `typecode` is the Analysis-level TOC typecode (`PYMODULE` or `DATA`)
547
+ logical_toc = []
548
+ code_dict = {}
549
+ extract_path = pyzpath + EXTRACT_SUFFIX
550
+
551
+ def compile_item(name, filename):
552
+ fullpath = os.path.join(obfpath, filename)
553
+ if not os.path.exists(fullpath):
554
+ fullpath = fullpath + 'c'
555
+ if os.path.exists(fullpath):
556
+ logger.info('replace item "%s"', name)
557
+ with open(fullpath, 'r', encoding='utf-8') as f:
558
+ co = compile(f.read(), '<frozen %s>' % name, 'exec')
559
+ code_dict[name] = co
560
+ else:
561
+ fullpath = os.path.join(extract_path, filename + 'c')
562
+ with open(fullpath, 'rb') as f:
563
+ f.seek(16)
564
+ code_dict[name] = marshal.load(f)
565
+ return fullpath
566
+
567
+ fullpath = os.path.join(obfpath, rtname, '__init__.py')
568
+ logical_toc.append((rtname, fullpath, 'PYMODULE'))
569
+ compile_item(rtname, os.path.join(rtname, '__init__.py'))
570
+
571
+ for name, (typecode, offset, length) in pyztoc.items():
572
+ ptname = name.replace('..', '__').replace('.', os.path.sep)
573
+ pytype = 'PYMODULE'
574
+ if typecode == PYZ_ITEM_PKG:
575
+ fullpath = compile_item(name, os.path.join(ptname, '__init__.py'))
576
+ elif typecode == PYZ_ITEM_MODULE:
577
+ fullpath = compile_item(name, ptname + '.py')
578
+ elif typecode == PYZ_ITEM_DATA:
579
+ fullpath = os.path.join(extract_path, ptname)
580
+ pytype = 'DATA'
581
+ elif typecode == PYZ_ITEM_NSPKG:
582
+ fullpath = compile_item(name, os.path.join(ptname, '__init__.py'))
583
+ fullpath = '-'
584
+ else:
585
+ raise ValueError('unknown PYZ item type "%s"' % typecode)
586
+ logical_toc.append((name, fullpath, pytype))
587
+
588
+ # It seems PyInstaller 6.0+ no keyword parameter: cipher
589
+ ZlibArchiveWriter(pyzpath, logical_toc, code_dict, cipher=cipher)
590
+
591
+
592
+ def repack_carchive(executable, pkgfile, buildpath, obfpath, rtentry):
593
+ pkgarch = CArchiveReader2(executable)
594
+ with open(executable, 'rb') as fp:
595
+ *_, pylib_name = pkgarch.get_cookie_info(fp)
596
+ logical_toc = pkgarch.get_logical_toc(buildpath, obfpath)
597
+ if rtentry is not None:
598
+ logical_toc.append(rtentry)
599
+ pylib_name = pylib_name.strip(b'\x00').decode('utf-8')
600
+ CArchiveWriter2(pkgarch, pkgfile, logical_toc, pylib_name)
601
+
602
+
603
+ def repack_executable(executable, buildpath, obfpath, rtentry, codesign=None):
604
+ pkgname = 'PKG-patched'
605
+
606
+ logger.info('repacking PKG "%s"', pkgname)
607
+ pkgfile = os.path.join(buildpath, pkgname)
608
+ repack_carchive(executable, pkgfile, buildpath, obfpath, rtentry)
609
+
610
+ logger.info('repacking EXE "%s"', executable)
611
+
612
+ if is_darwin:
613
+ import PyInstaller.utils.osx as osxutils
614
+ if hasattr(osxutils, 'remove_signature_from_binary'):
615
+ logger.info("remove signature(s) from EXE")
616
+ osxutils.remove_signature_from_binary(executable)
617
+
618
+ if is_linux:
619
+ logger.info('replace section "pydata" with "%s"', pkgname)
620
+ check_call(['objcopy', '--update-section', 'pydata=%s' % pkgfile,
621
+ executable])
622
+ else:
623
+ reader = CArchiveReader2(executable)
624
+ logger.info('replace PKG with "%s"', pkgname)
625
+ with open(executable, 'r+b') as outf:
626
+ info = reader.get_cookie_info(outf)
627
+ offset = os.fstat(outf.fileno()).st_size - info[1]
628
+ # Keep bootloader
629
+ outf.seek(offset, os.SEEK_SET)
630
+
631
+ # Write the patched archive
632
+ with open(pkgfile, 'rb') as infh:
633
+ shutil.copyfileobj(infh, outf, length=64*1024)
634
+
635
+ outf.truncate()
636
+
637
+ if is_darwin:
638
+ # Fix Mach-O header for codesigning on OS X.
639
+ logger.info('fixing EXE for code signing')
640
+ import PyInstaller.utils.osx as osxutils
641
+ osxutils.fix_exe_for_code_signing(executable)
642
+ # Since PyInstaller 4.4
643
+ if hasattr(osxutils, 'sign_binary'):
644
+ logger.info("re-signing the EXE")
645
+ osxutils.sign_binary(executable, identity=codesign)
646
+
647
+ elif is_win:
648
+ # Set checksum to appease antiviral software.
649
+ from PyInstaller.utils.win32 import winutils
650
+ if hasattr(winutils, 'set_exe_checksum'):
651
+ winutils.set_exe_checksum(executable)
652
+
653
+ logger.info('generate patched bundle "%s" successfully', executable)
654
+
655
+
656
+ class BundleRepacker:
657
+ """Deprecated method since v8.5.8"""
658
+
659
+ def __init__(self, ctx, executable, inputs, output):
660
+ self.ctx = ctx
661
+ self.executable = executable
662
+ self.output = os.path.normpath(output) if output else 'dist'
663
+ self.buildpath = ctx.pack_basepath
664
+ self.codesign = ctx.cfg['pack'].get('codesign_identify', None)
665
+ self.extract_carchive(executable, self.buildpath)
666
+
667
+ def check(self):
668
+ try:
669
+ from PyInstaller import __version__ as pyi_version
670
+ major = int(pyi_version.split('.')[0])
671
+ except Exception as e:
672
+ logger.warning("can't get PyInstaller version: %s", str(e))
673
+ pyi_version = 'unknown'
674
+ major = 6
675
+
676
+ if major > 5:
677
+ logger.info(
678
+ 'Please check documentation `insight into pack command`'
679
+ 'to find solutions or downgrade PyInstaller to version 5')
680
+ raise NotImplementedError(
681
+ "PyInstaller %s isn't supported" % pyi_version)
682
+
683
+ def extract_carchive(self, executable, buildpath, clean=True):
684
+ logger.info('extracting bundle "%s"', executable)
685
+ if os.path.exists(self.buildpath):
686
+ shutil.rmtree(self.buildpath)
687
+ os.makedirs(self.buildpath)
688
+
689
+ contents = []
690
+ pkgarch = CArchiveReader2(executable)
691
+ pkgtoc = pkgarch.get_toc()
692
+
693
+ with open(executable, 'rb') as fp:
694
+ *_, pylib_name = pkgarch.get_cookie_info(fp)
695
+ self.pylib_name = pylib_name.strip(b'\x00').decode('utf-8')
696
+ logger.debug('pylib_name is "%s"', self.pylib_name)
697
+
698
+ for name, toc_entry in pkgtoc.items():
699
+ logger.debug('extract %s', name)
700
+ *_, typecode = toc_entry
701
+
702
+ if typecode == PKG_ITEM_PYZ:
703
+ pyzarch = pkgarch.open_pyzarchive(name)
704
+ self.pyztoc = pyzarch.toc
705
+ contents.append(extract_pyzarchive(name, pyzarch, buildpath))
706
+
707
+ self.contents = contents
708
+ self.one_file_mode = len(pkgtoc) > 10 and not any([
709
+ x.name == 'base_library.zip'
710
+ for x in os.scandir(os.path.dirname(executable))])
711
+ logger.debug('one file mode is %s', bool(self.one_file_mode))
712
+
713
+ def build(self):
714
+ obfpath = os.path.normpath(self.ctx.pack_obfpath)
715
+ self.repack(obfpath, self.ctx.runtime_package_name)
716
+
717
+ def repack(self, obfpath, rtname, entry=None):
718
+ buildpath = self.buildpath
719
+ executable = self.executable
720
+ codesign = self.codesign
721
+ logger.info('repacking bundle "%s"', executable)
722
+
723
+ obfpath = os.path.normpath(obfpath)
724
+ logger.info('obfuscated scripts at "%s"', obfpath)
725
+
726
+ name, ext = os.path.splitext(os.path.basename(executable))
727
+ entry = name if entry is None else entry
728
+ logger.info('entry script name is "%s.py"', entry)
729
+
730
+ rtpath = os.path.join(obfpath, rtname)
731
+ logger.debug('runtime package at %s', rtpath)
732
+ for item in self.contents:
733
+ if item.endswith(EXTRACT_SUFFIX):
734
+ pyzpath = item[:-len(EXTRACT_SUFFIX)]
735
+ logger.info('repacking "%s"', os.path.basename(pyzpath))
736
+ repack_pyzarchive(pyzpath, self.pyztoc, obfpath, rtname)
737
+
738
+ for x in os.listdir(rtpath):
739
+ ext = os.path.splitext(x)[-1]
740
+ if x.startswith('pyarmor_runtime') and ext in ('.so', '.pyd'):
741
+ rtbinary = os.path.join(rtpath, x)
742
+ rtbinname = os.path.join(rtname, x)
743
+ break
744
+ else:
745
+ raise RuntimeError('no pyarmor runtime files found')
746
+
747
+ if is_darwin:
748
+ # Not required since 8.3.0
749
+ # from PyInstaller.depend import dylib
750
+ # self._fixup_darwin_rtbinary(rtbinary, self.pylib_name)
751
+ # logger.debug('mac_set_relative_dylib_deps "%s"', rtbinname)
752
+ # dylib.mac_set_relative_dylib_deps(rtbinary, rtbinname)
753
+
754
+ import PyInstaller.utils.osx as osxutils
755
+ # Since PyInstaller 4.4
756
+ if hasattr(osxutils, 'sign_binary'):
757
+ logger.info('re-signing "%s"', os.path.basename(rtbinary))
758
+ osxutils.sign_binary(rtbinary, identity=codesign)
759
+
760
+ rtentry = (rtbinname, rtbinary, 1, 'b') if self.one_file_mode else None
761
+ if not self.one_file_mode:
762
+ dest = os.path.join(os.path.dirname(executable), rtname)
763
+ os.makedirs(dest, exist_ok=True)
764
+ shutil.copy2(rtbinary, dest)
765
+
766
+ repack_executable(executable, buildpath, obfpath, rtentry, codesign)
767
+
768
+ def _fixup_darwin_rtbinary(self, rtbinary, pylib_name):
769
+ '''Unused since Pyarmor 8.3.0'''
770
+ from sys import version_info as pyver
771
+ from subprocess import check_output
772
+ pylib = os.path.normpath(os.path.join('@rpath', pylib_name))
773
+ output = check_output(['otool', '-L', rtbinary])
774
+ for line in output.splitlines():
775
+ if line.find(b'libpython%d.%d.dylib' % pyver[:2]) > 0:
776
+ reflib = line.split()[0].decode()
777
+ if reflib.endswith(pylib_name):
778
+ return
779
+ break
780
+ elif line.find(pylib.encode()) > 0:
781
+ return
782
+ # Only for debug
783
+ elif line.find(b'/Python ') > 0:
784
+ return
785
+ else:
786
+ logger.warning('fixup dylib failed, no CPython library found')
787
+
788
+ cmdlist = ['install_name_tool', '-change', reflib, pylib, rtbinary]
789
+ try:
790
+ logger.info('%s', ' '.join(cmdlist))
791
+ check_call(cmdlist, stdout=DEVNULL, stderr=DEVNULL)
792
+ except Exception as e:
793
+ logger.warning('%s', e)