at backyard

Color my life with the chaos of trouble.

cx_Freezeを使っていて、error: Multiple top-level packages discovered in a flat-layoutというエラーが出たときの対応手段

目次

今回の経緯

ツイッターで呟いた下記の内容になる。

割とこの問題、個人的には影響範囲大きそうだと感じているが、検索掛けても英語でこれに関する問題がちょいちょい引っかかるぐらい。

私の場合はcx_Freezeを利用していく中で、この問題にぶつかったが、cx_Freeze内でもまだそこまで大きな影響はなさそうだった。

もしかしたら自分の書いたソースコードがただ単に良くないだけで、実は皆ここについて特に影響なかったのか?という可能性に怯えながらも、この記事を備忘録として書き残しておくことにした。

tkinterで作ったノートアプリ

今回遭遇した問題はcx_Freezeでビルドする際に発生した問題だったが、そちらのプロジェクトはclosedなものなので細かな修正内容は書けない。

ただ、以前tkinterで作成していたノートアプリでも同様の事象が発生することから、今回こちらのプロジェクトを例にして修正した内容について書いていく。
修正したコミットログも最後に貼っておくので、そちらが参考になるかと思う。

shinshin86.hateblo.jp

error: Multiple top-level packages discovered in a flat-layout というエラー

環境

まず最初に自身の環境を下記に貼る。

利用しているのはm1 macbook airでバージョンは以下。

macOS: 11.6.5

Pythonはconda上で動いているもの。 ここらへんの経緯は下に書いた。
(私の記憶だとm1だとcondaを使わないとcx_Freezeは動かない。が、今は違うかも。)

shinshin86.hateblo.jp

$ python --version
Python 3.9.12


$ pip show cx-Freeze
Name: cx-Freeze
Version: 6.10
・
・
・

setuptoolsのバージョンも出そうと思ったけど、直接インストールしていないパッケージのバージョンってどうやって出せばいいんだっけ?
cx_Freezeに依存していると思うので、ひとまずソースコードを抜き出した。

setuptools>=59.0.1,<=60.10.0

https://github.com/marcelotduarte/cx_Freeze/blob/main/requirements.txt#L5

エラー再現と原因

問題再現のために、下記のソースコードを利用する。
(これは今回の修正を加える前のversion)

github.com

このバージョンで、

python setup.py bdist_mac

を実行すると、下記のようなエラーが出力される。

error: Multiple top-level packages discovered in a flat-layout: ['demo', 'view', 'model'].

To avoid accidental inclusion of unwanted files or directories,
setuptools will not proceed with this build.

If you are trying to create a single distribution with multiple packages
on purpose, you should not rely on automatic discovery.
Instead, consider the following options:

1. set up custom discovery (`find` directive with `include` or `exclude`)
2. use a `src-layout`
3. explicitly set `py_modules` or `packages` with a list of names

To find more information, look for "package discovery" on setuptools docs.

これはsetuptoolsのAutomatic discoveryという機能によるものとなっている。

https://setuptools.pypa.io/en/latest/userguide/package_discovery.html#automatic-discovery

おそらくcx_Freeze内部で使っているsetuptools側の処理でこういうエラーが出ているものかと思われる。
(修正内容などを元に私自身が推測しているだけ。cx_Freeze内部のソースコードは追っていない)

で、こちらのAutomatic discoveryを回避するために、Custom discoveryという機能がsetuptoolsにはある。

https://setuptools.pypa.io/en/latest/userguide/package_discovery.html#custom-discovery

ここに記載されたドキュメントを参考に修正を行う。

実際の修正内容

今回修正対象となるソースコードはcx_Freezeのビルド設定が記載された setup.py となる。

で、今回起きた問題というのは、

error: Multiple top-level packages discovered in a flat-layout: ['demo', 'view', 'model']

というように複数のトップレベルパッケージが検出されたことによるもので、今回実施する解決内容は、

1. set up custom discovery (`find` directive with `include` or `exclude`)

上にも書いたとおり、このcustom discoveryを設定していくことにする。

設定方法は下記のドキュメントを参考にすれば問題ない。

https://setuptools.pypa.io/en/latest/userguide/package_discovery.html#custom-discovery

今回の場合検出されたトップレベルパッケージが

  • demo'
  • view
  • model

の3つ。

ただし、demo自体はアプリのデモ画像が入ったディレクトリであり、本体にとって必要なパッケージではないため、こいつはビルドには含めない形で設定していく。

というわけで設定してみたdiffはこちら。

diff --git a/setup.py b/setup.py
index dfdd8f8..abeb321 100644
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,6 @@
 import sys
 from cx_Freeze import setup, Executable
+from setuptools import find_packages

 base = None
 if sys.platform == "win32":
@@ -12,4 +13,8 @@ setup(
     version="0.0.1",
     description="tkinter note app",
     executables=executables,
+    packages=find_packages(
+        include=["view", "model"],
+        exclude=["demo"]
+    )
 )

これで下記のコマンドでのビルドが通ることが確認できる。

python setup.py bdist_mac

一応これで問題は解決したのだが、もうちょい検証してみることにした。

検証:別に設定しなくてもcx_Freeze側でよしなにしている?

その後、色々試していると、別に下記のように何も設定しなくても、ビルドに成功できた。。

まあ、ここについてはcx_Freeze側でいい感じにビルドしているのかもしれない。。。あくまで推測ですが。

ここらへんはよく分からん挙動ですね。

diff --git a/setup.py b/setup.py
index dfdd8f8..7555bb1 100644
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,6 @@
 import sys
 from cx_Freeze import setup, Executable
+from setuptools import find_packages

 base = None
 if sys.platform == "win32":
@@ -12,4 +13,8 @@ setup(
     version="0.0.1",
     description="tkinter note app",
     executables=executables,
+    packages=find_packages(
+        include=[],
+        exclude=[]
+    )
 )

ちなみにこのようにdemoをincludeにふくめてもビルドのパッケージ内にはdemoディレクトリは見当たらない。

diff --git a/setup.py b/setup.py
index dfdd8f8..e4a8191 100644
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,6 @@
 import sys
 from cx_Freeze import setup, Executable
+from setuptools import find_packages

 base = None
 if sys.platform == "win32":
@@ -12,4 +13,8 @@ setup(
     version="0.0.1",
     description="tkinter note app",
     executables=executables,
+    packages=find_packages(
+        include=["demo", "view", "model"],
+        exclude=[]
+    )
 )

これも、やはりcx_Freeze側でよしなにしている?
(まあ、元々こういう設定は行わなくともビルドは出来ていたわけだし...)

というわけで以上となります。

結論:tkinterノートアプリの最終的なコミット内容

で、結局、ノートアプリの方は find_packages には何も設定しないやり方でコミットすることにしました。

find_packagesinclude に設定すると、それらのディレクトリが別のディレクトリにも出るんですよね。
( build/lib 配下に出力される )

でも、それはビルドしたアプリにとっては特に不要なものなので、たぶんここらへんはcx_Freeze側の機能とsetuptools側でうまく噛み合っていない気がしました。

そのため、setuptoolsのエラー回避という名目だけで find_packages を設定したような形になります。

github.com

というわけで、以上備忘録でした。