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/__init__.py +46 -0
- pyarmor/cli/__main__.py +812 -0
- pyarmor/cli/batch.py +62 -0
- pyarmor/cli/bootstrap.py +207 -0
- pyarmor/cli/bug.py +137 -0
- pyarmor/cli/command.py +563 -0
- pyarmor/cli/config.py +306 -0
- pyarmor/cli/context.py +790 -0
- pyarmor/cli/core.data.1 +0 -0
- pyarmor/cli/core.data.2 +0 -0
- pyarmor/cli/core.data.3 +0 -0
- pyarmor/cli/default.cfg +564 -0
- pyarmor/cli/docker.py +192 -0
- pyarmor/cli/generate.py +256 -0
- pyarmor/cli/hdinfo.py +81 -0
- pyarmor/cli/merge.py +232 -0
- pyarmor/cli/mixer.py +109 -0
- pyarmor/cli/model.py +460 -0
- pyarmor/cli/plugin.py +292 -0
- pyarmor/cli/project.py +1281 -0
- pyarmor/cli/public_capsule.zip +0 -0
- pyarmor/cli/register.py +1093 -0
- pyarmor/cli/repack.py +793 -0
- pyarmor/cli/resource.py +265 -0
- pyarmor/cli/shell.py +481 -0
- pyarmor_cli-9.2.5.dist-info/METADATA +74 -0
- pyarmor_cli-9.2.5.dist-info/RECORD +30 -0
- pyarmor_cli-9.2.5.dist-info/WHEEL +6 -0
- pyarmor_cli-9.2.5.dist-info/entry_points.txt +3 -0
- pyarmor_cli-9.2.5.dist-info/top_level.txt +1 -0
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)
|