diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8d1ba77a7..eb72ca235 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -221,6 +221,18 @@ jobs: run: | install_name_tool -add_rpath @executable_path/. build/run.dist/run + # NOTE: This task should ideally be done by Nuitka in the `Build run.py` step. + # Please remove this step when you have solved the problem with Nuitka. + - name: Copy the missing .dylib files into the distribution + run: | + python build_util/macos/copy_missing_dylibs.py build/run.dist/ + + # NOTE: This task should ideally be done by Nuitka in the `Build run.py` step. + # Please remove this step when you have solved the problem with Nuitka. + - name: Fix the rpaths of the .dylib files in the distribution + run: | + python build_util/macos/fix_rpaths.py build/run.dist/ + # FIXME: versioned name may be useful; but # actions/download-artifact and dawidd6/download-artifact do not support # wildcard / forward-matching yet. diff --git a/build_util/macos/build_util_macos/__init__.py b/build_util/macos/build_util_macos/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/build_util/macos/build_util_macos/shlib_tools.py b/build_util/macos/build_util_macos/shlib_tools.py new file mode 100644 index 000000000..ae53979f0 --- /dev/null +++ b/build_util/macos/build_util_macos/shlib_tools.py @@ -0,0 +1,93 @@ +""" +macOSにおいて共有ライブラリを操作するためのツールをまとめたモジュール +""" + +import subprocess +from pathlib import Path +from typing import List + + +def get_dylib_paths(base_path: Path) -> List[Path]: + """base_path以下の全てのサブディレクトリにあるdylibファイルのリストを返す""" + return list(base_path.glob("**/*.dylib")) + + +def get_rpaths(shared_lib_path: Path) -> List[Path]: + """引数で指定された共有ライブラリのrpathのリストを返す""" + proc = subprocess.run(["otool", "-L", str(shared_lib_path)], stdout=subprocess.PIPE) + output = proc.stdout.decode("utf-8") + paths = [ + Path(line.lstrip().split(" ", maxsplit=1)[0]) + for line in output.splitlines()[1:] + ] + # 得られたパスのリストのうち、共有ライブラリ自体とライブラリ名が同じものは + # rpath ではなく install ID というものなので除外 + return [ + path + for path in paths + if path.name.split(".")[0] != shared_lib_path.name.split(".")[0] + ] + + +def is_distributable_rpath(rpath: Path) -> bool: + """開発環境にインストールされたパッケージに依存しないrpathかどうか""" + # 以下のプレフィックスで始まるrpathは配布に際して問題がない + # - プレースホルダ。実行時に自動で解決される + # - @executable_path/ + # - @loader_path/ + # - @rpath/ + # - システム標準のライブラリがあるディレクトリ + # - /usr/lib/ + # - /System/Library/Frameworks/ + # - /System/Library/PrivateFrameworks/ + DISTRIBUTABLE_PREFIXES = [ + "@executable_path/", + "@loader_path/", + "@rpath/", + "/usr/lib/", + "/System/Library/Frameworks/", + "/System/Library/PrivateFrameworks/", + ] + result = False + + for prefix in DISTRIBUTABLE_PREFIXES: + if str(rpath).startswith(prefix): + result = True + break + else: + continue + + return result + + +def change_rpath(old_rpath: Path, new_rpath: Path, dylib_path: Path, base_path: Path): + """dylib_pathで指定されたdylibのrpathを、old_rpathから、new_rpath(base_pathからの相対パスに変換したもの)に変更する""" + relative_new_rpath = new_rpath.relative_to(base_path) + subprocess.run( + [ + "install_name_tool", + "-change", + old_rpath, + "@rpath/" + str(relative_new_rpath), + dylib_path, + ] + ) + + +class SharedLib: + """共有ライブラリの情報""" + + __path: Path + __rpaths: List[Path] + + def __init__(self, shared_lib_path: Path): + self.__path = shared_lib_path + self.__rpaths = get_rpaths(shared_lib_path) + + @property + def path(self) -> Path: + return self.__path + + def get_non_distributable_rpaths(self) -> List[Path]: + """rpathのうち、開発環境に依存しているもののリスト""" + return [rpath for rpath in self.__rpaths if not is_distributable_rpath(rpath)] diff --git a/build_util/macos/copy_missing_dylibs.py b/build_util/macos/copy_missing_dylibs.py new file mode 100644 index 000000000..cd1a81fb0 --- /dev/null +++ b/build_util/macos/copy_missing_dylibs.py @@ -0,0 +1,54 @@ +""" +配布物内の.dylibファイルの不足を解消するためのスクリプト + +引数で指定したbase_directory以下にある.dylibファイルのrpathをチェックし、 +rpathの指す.dylibファイルがbase_directory以下に存在しなかった場合、 +rpathの指している場所からその.dylibファイルをbase_directory直下へとコピーする。 +""" + +import argparse +import shutil +import sys +from pathlib import Path +from typing import List, Set + +from build_util_macos.shlib_tools import SharedLib, get_dylib_paths + +parser = argparse.ArgumentParser() +parser.add_argument( + "base_directory", help="copy the missing dylibs under base_directory", type=str +) +args = parser.parse_args() +base_dir_path = Path(args.base_directory) + +if not (base_dir_path.exists() and base_dir_path.is_dir()): + print("could not find the directory:", str(base_dir_path), file=sys.stderr) + exit(1) + +# base_dir_path以下の全てのサブディレクトリを探索して得たdylibのリスト +dylib_paths: List[Path] = get_dylib_paths(base_dir_path) +# 全てのdylibのファイル名のリスト +dylib_names: List[str] = [path.name for path in dylib_paths] + +# 開発環境に依存したrpathを持つdylibのリスト +non_distributable_dylibs: List[SharedLib] = [] +for dylib_path in dylib_paths: + lib = SharedLib(dylib_path) + if lib.get_non_distributable_rpaths(): + non_distributable_dylibs.append(lib) + +# 開発環境に依存したrpathの集合 +non_distributable_rpaths: Set[Path] = set() +for dylib in non_distributable_dylibs: + rpaths: Set[Path] = set([rpath for rpath in dylib.get_non_distributable_rpaths()]) + non_distributable_rpaths = non_distributable_rpaths.union(rpaths) + +# rpathが指しているdylibのうち、base_dir_path以下に存在しないもののリスト +external_dylib_paths: List[Path] = [] +for rpath in non_distributable_rpaths: + if not (rpath.name in dylib_names): + external_dylib_paths.append(rpath) + +# 不足しているdylibをbase_dir_path直下にコピー +for dylib_path in external_dylib_paths: + shutil.copy(dylib_path, base_dir_path, follow_symlinks=True) diff --git a/build_util/macos/fix_rpaths.py b/build_util/macos/fix_rpaths.py new file mode 100644 index 000000000..c2bd0f459 --- /dev/null +++ b/build_util/macos/fix_rpaths.py @@ -0,0 +1,67 @@ +""" +配布物内の.dylibファイルのrpathをどのようなユーザー環境においても有効になるように修正するスクリプト + +引数で指定したbase_directory以下にある.dylibファイルのrpathをチェックし、 +開発環境に依存した(配布先の環境に存在することが保証されていない)rpathであった場合、 +base_directory以下の.dylibファイルを相対パスで指すように変更する。 +(base_directory以下の.dylibファイルに不足がないことを前提とする。) +""" + +import argparse +import sys +from pathlib import Path +from typing import List, Set + +from build_util_macos.shlib_tools import SharedLib, change_rpath, get_dylib_paths + +parser = argparse.ArgumentParser() +parser.add_argument( + "base_directory", help="fix the rpaths of the dylibs under base_directory", type=str +) +args = parser.parse_args() +base_dir_path = Path(args.base_directory) + +if not (base_dir_path.exists() and base_dir_path.is_dir()): + print("could not find the directory:", str(base_dir_path), file=sys.stderr) + exit(1) + +# base_dir_path以下の全てのサブディレクトリを探索して得たdylibのリスト +internal_dylib_paths: List[Path] = get_dylib_paths(base_dir_path) +# 全てのdylibのファイル名のリスト +internal_dylib_names: List[str] = [path.name for path in internal_dylib_paths] + +# 開発環境に依存したrpathを持つdylibのリスト +non_distributable_dylibs: List[SharedLib] = [] +for internal_dylib_path in internal_dylib_paths: + lib = SharedLib(internal_dylib_path) + if lib.get_non_distributable_rpaths(): + non_distributable_dylibs.append(lib) + +# 開発環境に依存したrpathの集合 +non_distributable_rpaths: Set[Path] = set() +for dylib in non_distributable_dylibs: + rpaths: Set[Path] = set([rpath for rpath in dylib.get_non_distributable_rpaths()]) + non_distributable_rpaths = non_distributable_rpaths.union(rpaths) + +# rpathが指しているdylibのうち、base_dir_path以下に存在しないもののリスト +external_dylib_paths: List[Path] = [] +for rpath in non_distributable_rpaths: + if not (rpath.name in internal_dylib_names): + external_dylib_paths.append(rpath) + +# base_dir_path以下でdylibが不足している場合は、不足しているdylibを表示して終了 +if external_dylib_paths: + print( + f"following dylibs not found under base_dir_path ({str(base_dir_path)}):", + file=sys.stderr, + ) + for path in external_dylib_paths: + print(f"\t{path.name}", file=sys.stderr) + exit(1) + +# 開発環境に依存したrpathを、base_dir_path以下のdylibを指すように変更 +for dylib in non_distributable_dylibs: + for rpath in dylib.get_non_distributable_rpaths(): + for internal_dylib_path in internal_dylib_paths: + if internal_dylib_path.name == rpath.name: + change_rpath(rpath, internal_dylib_path, dylib.path, base_dir_path)