Python Click ライブラリを使用して)でビルドしているCLIアプリでPyInstallerを実行しようとしています。PyInstallerを使用してプロジェクトをビルドできません。PyInstallerには Recipe Setuptools Entry Point というタイトルのGitHub wikiのドキュメントには、このプロジェクトで使用しているsetuptools
パッケージでPyInstallerを使用する方法に関する情報が記載されています。しかし、 pyinstaller --onefile main.spec
を実行すると、ベースモジュールが見つかりません。
私の質問は、問題は単に私が持っているフォルダ構造の問題ですか? Recipe Setuptools Entry Point は特定のファイル構造を想定していますか?
関連情報
Pyinstallerの出力
184 INFO: PyInstaller: 3.3.1
184 INFO: Python: 3.6.4
189 INFO: Platform: Darwin-16.7.0-x86_64-i386-64bit
193 INFO: UPX is available.
Traceback (most recent call last):
File "/usr/local/bin/pyinstaller", line 11, in <module>
sys.exit(run())
File "/usr/local/lib/python3.6/site-packages/PyInstaller/__main__.py", line 94, in run
run_build(pyi_config, spec_file, **vars(args))
File "/usr/local/lib/python3.6/site-packages/PyInstaller/__main__.py", line 46, in run_build
PyInstaller.building.build_main.main(pyi_config, spec_file, **kwargs)
File "/usr/local/lib/python3.6/site-packages/PyInstaller/building/build_main.py", line 791, in main
build(specfile, kw.get('distpath'), kw.get('workpath'), kw.get('clean_build'))
File "/usr/local/lib/python3.6/site-packages/PyInstaller/building/build_main.py", line 737, in build
exec(text, spec_namespace)
File "<string>", line 40, in <module>
File "<string>", line 26, in Entrypoint
File "/usr/local/lib/python3.6/site-packages/pkg_resources/__init__.py", line 582, in get_entry_info
return get_distribution(dist).get_entry_info(group, name)
File "/usr/local/lib/python3.6/site-packages/pkg_resources/__init__.py", line 564, in get_distribution
dist = get_provider(dist)
File "/usr/local/lib/python3.6/site-packages/pkg_resources/__init__.py", line 436, in get_provider
return working_set.find(moduleOrReq) or require(str(moduleOrReq))[0]
File "/usr/local/lib/python3.6/site-packages/pkg_resources/__init__.py", line 984, in require
needed = self.resolve(parse_requirements(requirements))
File "/usr/local/lib/python3.6/site-packages/pkg_resources/__init__.py", line 870, in resolve
raise DistributionNotFound(req, requirers)
pkg_resources.DistributionNotFound: The 'myapp' distribution was not found and is required by the application
main.spec
のmain.py
ファイルは、私のCLIアプリのエントリポイントです
block_cipher = None
def Entrypoint(dist, group, name,
scripts=None, pathex=None, hiddenimports=None,
hookspath=None, excludes=None, runtime_hooks=None):
import pkg_resources
# get toplevel packages of distribution from metadata
def get_toplevel(dist):
distribution = pkg_resources.get_distribution(dist)
if distribution.has_metadata('top_level.txt'):
return list(distribution.get_metadata('top_level.txt').split())
else:
return []
hiddenimports = hiddenimports or []
packages = []
for distribution in hiddenimports:
packages += get_toplevel(distribution)
scripts = scripts or []
pathex = pathex or []
# get the entry point
ep = pkg_resources.get_entry_info(dist, group, name)
# insert path of the Egg at the verify front of the search path
pathex = [ep.dist.location] + pathex
# script name must not be a valid module name to avoid name clashes on import
script_path = os.path.join(workpath, name + '-script.py')
print ("creating script for entry point", dist, group, name)
with open(script_path, 'w') as fh:
print("import", ep.module_name, file=fh)
print("%s.%s()" % (ep.module_name, '.'.join(ep.attrs)), file=fh)
for package in packages:
print ("import", package, file=fh)
return Analysis([script_path] + scripts, pathex, hiddenimports, hookspath, excludes, runtime_hooks)
a = Entrypoint('myapp', 'console_scripts', 'myapp')
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
exclude_binaries=True,
name='main',
debug=False,
strip=False,
upx=True,
console=True )
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
name='main')
仮想環境でpip3 install --editable .
を実行したときに生成されたmyapp
スクリプトの内容:
#!/some/path/to/myapp-cli/venv/bin/python3.6
# EASY-INSTALL-ENTRY-SCRIPT: 'myapp','console_scripts','myapp'
__requires__ = 'myapp'
import re
import sys
from pkg_resources import load_entry_point
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
sys.exit(
load_entry_point('myapp', 'console_scripts', 'myapp')()
)
そして最後に、私のリポジトリ構造:
myapp-cli/
├── README.md
├── myapp
│ ├── __init__.py
│ ├── main.py
│ ├── main.spec
│ ├── resources
│ │ ├── __init__.py
│ │ └── functions.py
│ ├── subcommands
│ │ ├── __init__.py
│ │ ├── config
│ │ │ ├── __init__.py
│ │ │ └── cli.py
│ │ ├── create
│ │ │ ├── __init__.py
│ │ │ └── cli.py
│ │ ├── destroy
│ │ │ ├── __init__.py
│ │ │ └── cli.py
│ │ └── switch
│ │ ├── __init__.py
│ │ └── cli.py
│ └── variables.py
├── requirements.txt
└── setup.py
そして私のsetup.py
ファイル:
from setuptools import find_packages
from setuptools import setup
import os
base_dir = os.path.dirname(__file__)
setup(
entry_points = '''
[console_scripts]
myapp=myapp.main:entry_point
''',
install_requires = [
'packageone==1.0',
'packagetwo==2.0',
],
name = "myapp",
packages=find_packages(),
setup_requires="setuptools",
version = "0.1",
)
最初:私はスティーブンの答えと、自分でいくつかの掘り下げを組み合わせて答えを見つけました。最後に、Stephenの最初の部分でトリックを実行しました:PYTHONPATH
変数を手動で追加/エクスポートします。実際には、次のようにpathex
関数のEntrypoint
を使用してこれを指定できます。
a = Entrypoint('myapp-cli',
'console_scripts',
'myapp',
pathex=['/some/path/to/myapp-cli/myapp', '/some/path/to/myapp-cli']
)
結局、myapp.main
は必要なくなりました。
2番目:PyInstallerでまだ問題がありましたnot単一のバイナリを生成します。私にとって、これはトリックをしました:
requirements.txt
またはinstall_requires
のsetup.py
に追加します。 https:// github。 com/pyinstaller/pyinstaller/archive/develop.Zip 。.spec
の--onefile
オプションを使用して、pyi-makespec
ファイルをpyi-makespec --onefile myapp.py
のように作成することもできます。これにより、すべてのパッケージが確実にバイナリにコンパイルされる.spec
ファイルが作成されます。結局、次のスペックファイルでうまくいき、完全に機能するバイナリを作成することができました:
# -*- mode: python -*-
block_cipher = None
def Entrypoint(dist, group, name,
scripts=None, pathex=None, hiddenimports=None,
hookspath=None, excludes=None, runtime_hooks=None):
import pkg_resources
# get toplevel packages of distribution from metadata
def get_toplevel(dist):
distribution = pkg_resources.get_distribution(dist)
if distribution.has_metadata('top_level.txt'):
return list(distribution.get_metadata('top_level.txt').split())
else:
return []
hiddenimports = hiddenimports or []
packages = []
for distribution in hiddenimports:
packages += get_toplevel(distribution)
scripts = scripts or []
pathex = pathex or []
# get the entry point
ep = pkg_resources.get_entry_info(dist, group, name)
# insert path of the Egg at the verify front of the search path
pathex = [ep.dist.location] + pathex
# script name must not be a valid module name to avoid name clashes on import
script_path = os.path.join(workpath, name + '-script.py')
print ("creating script for entry point", dist, group, name)
with open(script_path, 'w') as fh:
print("import", ep.module_name, file=fh)
print("%s.%s()" % (ep.module_name, '.'.join(ep.attrs)), file=fh)
for package in packages:
print ("import", package, file=fh)
return Analysis([script_path] + scripts, pathex, hiddenimports, hookspath, excludes, runtime_hooks)
a = Entrypoint('myapp-cli',
'console_scripts',
'myapp',
pathex=['/some/path/to/myapp-cli/myapp', '/some/path/to/myapp-cli']
)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='myapp',
debug=False,
strip=False,
upx=True,
runtime_tmpdir=None,
console=True )
最終的に、Golangは1ファイルのバイナリを箱からコンパイルするので、 Cobra for Golang のようなものを使用すると簡単に機能すると思います。ただし、Pythonを使用する場合は、これでうまくいくはずです。
このエラー:
pkg_resources.DistributionNotFound: 'myapp'ディストリビューションが見つからなかったため、アプリケーションに必要です
このパッケージがPYTHONPATH
にないことを示します。私はそれをWindowsで修正しました:
set PYTHONPATH=.
選択したOSに合わせて調整します。
パスの問題に加えて、次のものがあります。
setup(
entry_points = '''
[console_scripts]
myapp=myapp.main:entry_point
''',
a = Entrypoint('myapp', 'console_scripts', 'myapp')
Setup.pyによると、エントリポイントはmyapp
ではなくmyapp.main
のようです。だからあなたは必要になるかもしれません:
a = Entrypoint('myapp', 'console_scripts', 'myapp.main')
受け入れられた答えは私にとってはうまくいきませんでした。 Egg-info
ファイルを介して.spec
ディレクトリを追加する必要がありました。
Entrypoint
関数の呼び出しは次のようになります。
a = Entrypoint(
'PrintIt',
'console_scripts',
'printit',
datas=[('plugins/*.Egg', 'plugins/'),
('../PrintIt.Egg-info/*', 'PrintIt.Egg-info/')])
データファイルを追加する一般的な方法 は、Scott Crooksが ticked answer で推奨する方法でEntrypointにパッチを適用すると機能しなくなることに気づきました。 。私にとっては、a.datas
配列。 python3では、次のようになります。
...
a = Entrypoint(...)
from pathlib import Path
Path('/tmp/modulename/datafile.txt').write_text(Path('datafile.txt').read_text()))
a.datas.append('datafile.txt', '/tmp/modulename/datafile.txt', 'DATA')
pyz = PYZ(...)
...
多くの検索の後、このエラーは通常、プロジェクトのパッケージのメタデータ(つまり、バージョンが主要なものである)にアクセスしようとしたことが原因です。
パッケージのメタデータは通常、pkg_resources
または以前のdistutil
を使用して明示的にアクセスされるか、他のパッケージに隠されていることがよくあります(通常はパッケージバージョンにアクセスしようとします)。 Python v3.8から始まり、importlib.metadata
内のstdlibでも利用できるようになります。
その場合は、mypackage.Egg-info
フォルダー内の一部またはすべてのファイル、特にファイルPKG_INFO
を含める必要がありますが、すべてが必要になる場合があります。
これを行うには複数の方法がありますが、ここに私が好きないくつかがあります:
script.spec
ファイルを使用している場合は、チャールズの回答に従って、datas=
行を更新してこの情報を含めることができます。a = Analysis(['myscript.py'],
pathex=['C:\\path\\to\\mypackage'],
binaries=[],
datas=[('mypackage.Egg-info/*','mypackage.Egg-info')],
hook-mypackage.py
フックファイルを作成します。次の非常にシンプルでエレガントな行を使用します。
from PyInstaller.utils.hooks import copy_metadata
datas = copy_metadata('md2mat')
これをルートのpackage/repoフォルダーの新しいhooks
フォルダーに入れ、次にpyinstallerコマンドに以下を追加しました。
pyinstaller -F -y --additional-hooks-dir=hooks myscript.py
それはかなりうまくいき、古いメタデータパッケージから新しいimportlib.metadataに切り替えるときにcopy_metadata
関数が適切に維持されていると仮定すると、将来のPythonの更新を通じてうまく機能するはずです。
これが機能するようになれば、これが私のお気に入りかもしれません...
pyinstaller --add-data <SRC;DEST> myscript.py
このオプション--add-data
はヘルプ出力(pyinstaller --help
)に表示され、引数のフォーマットはSRC; DEST for Windowsであることを示しているため、Ithink他のメソッドのdatas=
形式と一致する必要がありますが、機能させることができませんでした。
私が正しいフォーマットに到達したと思う最も近いものは次のとおりです:
pyinstaller -F -y --add-data "mypackage.Egg-info/*;mypackage.Egg-info"
pyinstaller -F -y --add-data="mypackage.Egg-info/*;mypackage.Egg-info"
これらはコンパイルされますが、結果のexeは出力なしで実行されます。
PyInstaller Documentation に--add-data
オプションがありませんが、pyinstaller --help-commands
を実行すると表示されます。