4.3. Insight Into Pack Command
Pyarmor has no pack feature, it need call PyInstaller to pack the obfuscated script to final bundle, so first install PyInstaller:
$ pip install pyinstaller
PyInstaller will analysis script to find imported modules and packages, once the script is obfuscated, nothing could be found, the final bundle complains of missing module.
Pyarmor provides option --pack
to fix this problem, it supports the following values
onefile: pack the obfuscated script to onefile
onedir: pack the obfuscated script to onedir
specfile: one
.spec
file used by PyInstaller to generate bundle
Once it’s set, pyarmor will not only obfuscate the scripts, but also pack them to one bundle
4.3.1. Packing Scripts Automatically
Suppose our project tree like this:
project/
├── foo.py
├── foo.spec
├── util.py
└── joker/
├── __init__.py
├── card.py
├── queens.py
└── config.json
Let’s check what happens when the following commands are executed:
$ cd project
$ pyarmor gen --pack onefile foo.py
Pyarmor first call PyInstaller to analysis plain script foo.py to find all the imported moduels and packages
The imported module util and package joker are in the same path of foo.py, so Pyarmor will obfuscate foo.py, util.py and package joker by obfuscation options, and save them to path .pyarmor/pack/dist
For the other imported modules and packages, save them to hidden imports table
Finally pyarmor call PyInstaller again, pack all obfuscated scripts in .pyarmor/pack/dist and all the modules and packages in hidden imports table to one bundle.
Now let’s run the final bundle, it’s dist/foo or dist/foo.exe:
$ ls dist/foo
$ dist/foo
If need one folder bundle, just pass onedir to pack:
$ pyarmor gen --pack onedir foo.py
$ ls dist/foo
$ dist/foo/foo
4.3.1.1. Using specfile
In this project, there already has one foo.spec
which could be used to pack plain script to onefile. For example:
$ pyinstaller foo.spec
$ dist/foo
In this case, pass it to --pack
directly. For example:
$ pyarmor gen --pack foo.spec -r foo.py util.py joker/
What will Pyarmor do?
Pyarmor first obfuscates the scripts list in the command line, save them to .pyarmor/pack/dist
Next generates
foo.patched.spec
byfoo.spec
Finally call PyInstaller to pack the bundle by
foo.patched.spec
This patched specfile could replace plain scripts with obfuscated ones in .pyarmor/pack/dist when generating the bundle
Note
By this way, only listed scripts are obfuscated. If need obfuscate other used modules and packages, list all of them in command line.
4.3.1.2. Checking Obfuscated Scripts Have Been Packed
Add one line in the script foo.py
or joker/__init__.py
print('this is __pyarmor__', __pyarmor__)
If it’s not obfuscated, the final bundle will raise error. Because builtin name __pyarmor__
is only available in the obfuscated scripts.
4.3.1.3. Using More PyInstaller Options
If need extra PyInstaller options, using configuration item pack:pyi_options
. For example, reset it with one PyInstaller option -w
:
$ pyarmor cfg pack:pyi_options = " -w"
Note that there need one leading whitespace in the " -w"
, otherwise shell may complain of syntax error.
Let’s append another option -i
, it must be one whitespace between option -i
and its value, do not use =
. For example:
$ pyarmor cfg pack:pyi_options + " -i favion.ico"
Append another option:
$ pyarmor cfg pack:pyi_options + " --add-data joker/config.json:joker"
In Windows, maybe need use ;
as path separator instead of :
:
C:/User/test> pyarmor cfg pack:pyi_options + "--add-data joker/config.json;joker"
All of them could be done by one command:
$ pyarmor cfg pack:pyi_options = " -w -i favion.ico --add-data joker/config.json:joker"
See also
4.3.1.4. Using More Obfuscation Options
You can use any other obfuscation options to improve security. For example:
$ pyarmor gen --pack onefile --private foo.py
Anoter example, in Darwin, let obfuscated scripts work in both intel and Apple Silicon by extra option --platform darwin.x86_64,darwin.arm64
:
$ pyarmor cfg pack:pyi_options = "--target-architecture universal2"
$ pyarmor gen --pack onefile --platform darwin.x86_64,darwin.arm64 foo.py
Note that some of them may not work. For example, --restrict
can’t be used with --pack
.
4.3.2. Packing obfuscated scripts manually
If something is wrong with --pack
, or the final bundle doesn’t work, try to pack the obfuscated scripts manually.
You need to know how to using PyInstaller and using spec file, if not, learn it by yourself.
First obfuscate the script by Pyarmor. List all the scripts and folders need to be obfuscated after main script, other obfuscation options could be used, but no
-i
or--prefix
1:$ cd project/ $ pyarmor gen -O obfdist -r foo.py util.py joker/
Make sure the obfuscated script works:
$ python obfdist/foo.py
Then generate
foo.spec
by PyInstaller 2:$ pyi-makespec --onefile foo.py
Make sure it works and the final bundle works:
$ pyinstaller foo.spec $ dist/foo
Next patch
foo.spec
before linepyz = PYZ
, this is major work
# Pyarmor patch start:
srcpath = ''
obfpath = 'obfdist'
def apply_pyarmor_patch(srcpath, obfpath):
from PyInstaller.compat import is_win, is_cygwin
extname = 'pyarmor_runtime' + ('.pyd' if is_win or is_cygwin else '.so')
from glob import glob
rtpkg = glob(os.path.join(obfpath, '*', extname))
if len(rtpkg) != 1:
raise RuntimeError('No runtime package found')
rtpkg = os.path.basename(os.path.dirname(rtpkg[0]))
extpath = os.path.join(rtpkg, extname)
if hasattr(a.pure, '_code_cache'):
code_cache = a.pure._code_cache
else:
from PyInstaller.config import CONF
code_cache = CONF['code_cache'].get(id(a.pure))
# Make sure both of them are absolute paths
src = os.path.normcase(os.path.abspath(srcpath))
obf = os.path.abspath(obfpath)
n = len(src) + 1
count = 0
for i in range(len(a.scripts)):
if os.path.normcase(a.scripts[i][1]).startswith(src):
x = os.path.join(obf, a.scripts[i][1][n:])
if os.path.exists(x):
a.scripts[i] = a.scripts[i][0], x, a.scripts[i][2]
count += 1
if count == 0:
raise RuntimeError('No obfuscated script found')
for i in range(len(a.pure)):
if os.path.normcase(a.pure[i][1]).startswith(src):
x = os.path.join(obf, a.pure[i][1][n:])
if os.path.exists(x):
code_cache.pop(a.pure[i][0], None)
a.pure[i] = a.pure[i][0], x, a.pure[i][2]
a.pure.append((rtpkg, os.path.join(obf, rtpkg, '__init__.py'), 'PYMODULE'))
a.binaries.append((extpath, os.path.join(obf, extpath), 'EXTENSION'))
apply_pyarmor_patch(srcpath, obfpath)
# Pyarmor patch end.
# Before this line
# pyz = PYZ(...)
Finally generate bundle by this patched
foo.spec
, use option--clean
to to remove all cached files:$ pyinstaller --clean foo.spec
If following this example, please
Set
srcpath
to your path, in this example, it’s current pathSet
obfpath
to your real path, in this example, it’sobfdist
how to verify obfuscated scripts have been packed
Insert debug code in the main script or imported modules and packages. For example
print('this is __pyarmor__', __pyarmor__)
If it’s not obfuscated, the final bundle will raise error.
notes
4.3.3. Packing with PyInstaller Bundle
Deprecated since version 8.5.4: Use --pack
onefile
or onedir
instead.
The option --pack
also could accept an executable file generated by PyInstaller:
$ pyinstaller foo.py
$ pyarmor gen --pack dist/foo/foo foo.py
But only PyInstaller < 6.0 works by this method. If this option is set, pyarmor first obfuscates the scripts, then:
Unpacking this executable to a temporary folder
Replacing the scripts in bundle with obfuscated ones
Appending runtime files to the bundle in this temporary folder
Repacking this temporary folder to an executable file and overwrite the old
Important
Only listed scripts are obfuscated, if need obfuscate more scripts and sub packages, list all of them in command line. For example:
$ pyarmor gen --pack dist/foo/foo -r *.py dir1 dir2 ...
4.3.4. Segment fault in Apple M1
In Apple M1 if the final executable segment fault, please check codesign of runtime package:
$ codesign -v dist/foo/pyarmor_runtime_000000/pyarmor_runtime.so
And re-sign it if the code sign is invalid:
$ codesign -f -s dist/foo/pyarmor_runtime_000000/pyarmor_runtime.so
If you use --enable-bcc
or --enable-jit
to obfuscate the scripts, you need enable Allow Execution of JIT-compiled Code Entitlement
If your app doesn’t have the new signature format, or is missing the DER entitlements in the signature, you’ll need to re-sign the app on a Mac running macOS 11 or later, which includes the DER encoding by default.
If you’re unable to use macOS 11 or later to re-sign your app, you can re-sign it from the command-line in macOS 10.14 and later. To do so, use the following command to re-sign the MyApp.app app bundle with DER entitlements by using a signing identity named “Your Codesign Identity” stored in the keychain:
$ codesign -s "Your Codesign Identity" -f --preserve-metadata --generate-entitlement-der /path/to/MyApp.app