1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

macOSでHEASOFT/ftoolsが「ターミナルでは動くのにスクリプトで死ぬ」問題を潰す:DYLDサニタイズと再現性のある解決策

Last updated at Posted at 2025-12-30

はじめに

HEASOFT(ftools)を使った解析では、ftselect, ftmgtime, ftcopy などをシェルスクリプトから呼ぶことが多い。ところが macOS では、

  • ターミナルで手入力だと動く
  • 同じコマンドを .sh から呼ぶと dyld: Library not loaded で落ちる

という現象が現実的に起こる。

典型例:

  • 対話ターミナルでは DYLD_LIBRARY_PATH が設定されている
  • しかし #!/bin/sh#!/bin/bash で起動したスクリプト内では DYLD_LIBRARY_PATH=(空)
  • その結果、ftselectlibcfitsio を見つけられず Abort する

この問題は 「設定ミス」ではなく macOS のセキュリティ設計に起因することが多い。
本記事では、原因の正体と、Linux/macOS両対応で破綻しない実務的な解決策をまとめる。

よくわからん(?)エラーの例。この限りではないでしょうが。

dyld[11587]: Library not loaded: /Users/birby/heasoft-6.35/heasoft-6.35/x86_64-apple-darwin23.6.0/lib/libcfitsio.10.dylib Referenced from: <0EFFAC87-5252-34FD-B2F4-032BC0EBF7F6> /Users/XXXX/work/software/heasoft-6.35.1/x86_64-apple-darwin23.6.0/bin/ftselect Reason: tried: '/Users/birby/heasoft-6.35/heasoft-6.35/x86_64-apple-darwin23.6.0/lib/libcfitsio.10.dylib' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/Users/birby/heasoft-6.35/heasoft-6.35/x86_64-apple-darwin23.6.0/lib/libcfitsio.10.dylib' (no such file), '/Users/birby/heasoft-6.35/heasoft-6.35/x86_64-apple-darwin23.6.0/lib/libcfitsio.10.dylib' (no such file)

これは mac に HEASoft をバイナリで入れた場合に顕在化しやすいはずです。mac で Heasoft をバイナリで入れる方法は下記を参照ください。

xselectなどを使った時に、下記のようなよくわからんエラーが出て困る!!という人向けの解決方法についてです。

  Referenced from: <4FC240B7-DAF1-3F69-9CC4-289150E9DC66> /Users/yamada/work/software/heasoft-6.35.1/x86_64-apple-darwin23.6.0/bin/ffilecat
  Reason: tried: '/Users/birby/heasoft-6.35/heasoft-6.35/x86_64-apple-darwin23.6.0/lib/libcfitsio.10.dylib' (no such file)

などのエラーが出る場合です。これは、

otool -L `which xselect`

とやったときに、dylib が存在してないファイルを参照してることが原因です。

HEASoftバイナリの根本的な原因は、birbyというユーザーの直下に何かがあることを前提としてしまうところがあるので、sudoで英やっとシンボリックリンクを作成する、という邪道な解決方法もあります。。

% sudo mkdir -p /Users/birby/heasoft-6.35
% pwd
/Users/birby/heasoft-6.35
% sudo ln -s /Users/yamada/work/software/heasoft-6.35.1  heasoft-6.35  

のように、無理やりリンクを貼ってしまう。主には、cfitsioが動的にリンクを呼んでるところが切れてること多い気がするので、heasoft どの version でもほぼ大丈夫なはず。

現象:なぜ HEADAS は残るのに DYLD_LIBRARY_PATH だけ消えるのか

まず観測

親シェル(zsh)では:

echo $HEADAS
echo $DYLD_LIBRARY_PATH

がどちらも表示される。しかし、同じ環境から起動した bash の中では:

env | grep -E 'HEADAS|DYLD'

  • HEADAS=... は残る
  • DYLD_LIBRARY_PATH は出ない(消えている)

という差が出る。

これは「特殊変数」と「普通の変数」の差

  • HEADAS は普通の環境変数
    → 子プロセスに素直に継承される

  • DYLD_LIBRARY_PATH は macOS の動的リンカ dyld を制御する “危険度の高い” 環境変数
    → 条件によって OS 側で 無効化(サニタイズ) されることがある

DYLD_* は、悪意ある dylib の注入に使えるため、macOS では「安全でない起動経路」「保護対象のバイナリ」を経由すると、起動時に落とされることがある。
その結果、「親にはあるのに子で消える」が発生する。

重要:この挙動は Linux では基本的に起きない。
したがって「macだけ壊れる」「スクリプトだけ壊れる」になる。

なぜ「ターミナルからは動く」のか

ターミナルで ftselect を手打ちすると動く理由はたいてい次のどれか:

  1. ターミナル(zsh)の起動時に .zshrc が読み込まれ headas-init.sh が走っている
  2. そのときの DYLD_LIBRARY_PATH同一プロセス(zsh) の中で有効
  3. さらに ftselect 実行は zsh から直接 exec されるので、その環境が使える

一方、シェルスクリプトは #!/bin/sh / #!/bin/bash別プロセスとして起動される。
このとき macOS のサニタイズ条件に引っかかると DYLD_LIBRARY_PATH が消え、HEASOFT のバイナリが必要な dylib を見つけられなくなる。

設計指針:対話環境(.zshrc)とバッチ環境(スクリプト)を分離する

よくある誤解

.zshrc

source $HEADAS/headas-init.sh

と書いてあるから、スクリプトでも同じ環境が使えるはず、という期待。

現実

  • .zshrc は “対話的 zsh” 用
  • #!/bin/sh#!/bin/bash のスクリプトでは読まれない
  • さらに macOS では DYLD_* が “親→子”で落とされる場合がある

したがって、バッチ実行で再現性を担保したいなら:

スクリプトはスクリプト自身で環境を完結させる

が正しい。

最小で最強の対策:HEADAS から DYLD_LIBRARY_PATH を復元する

この1行(厳密には数行)が、最小のワークアラウンド:

if [ -n "${HEADAS:-}" ] && [ -z "${DYLD_LIBRARY_PATH:-}" ]; then
    export DYLD_LIBRARY_PATH="$HEADAS/lib"
fi

なぜこれが効くのか

  • HEADAS は普通の環境変数なので残りやすい
  • HEADAS/lib は HEASOFT の dylib(cfitsio など)が置かれている
  • つまり DYLD_LIBRARY_PATH「親から受け取る」のではなく「子の中で再構成」 している

macOS は「親→子で持ち込まれる DYLD」を警戒するが、子が自分で設定した DYLDまでは(多くの場合)止めない。実務的にはこれで解決することが多い(はず)。

「全部のスクリプトに呪文を入れたくない」問題と解決策

解決策A(推奨):共通ファイルを1つ作り、各スクリプトは1行だけ読む

heasoft_env.sh を作る:

heasoft_env.sh
# heasoft_env.sh
case "$(uname -s 2>/dev/null)" in
  Darwin)
    if [ -n "${HEADAS:-}" ] && [ -z "${DYLD_LIBRARY_PATH:-}" ]; then
      export DYLD_LIBRARY_PATH="$HEADAS/lib"
    fi
    ;;
  *)
    # Linux等では基本的に何もしない
    ;;
esac

各スクリプトには「呪文」ではなく、この1行だけ:

. "$(dirname "$0")/heasoft_env.sh"

ここで重要:.source の違い

  • . は POSIX 標準(shでも動く)
  • source は bash/zsh の拡張(shだと死ぬ)

Linux/macOS 両対応を狙うなら . が無難

dirname "$0" は何か

  • $0 は “このスクリプトのパス”
  • dirname "$0" は “そのディレクトリ”
  • よって、同じディレクトリにある heasoft_env.sh を確実に読める

より堅牢に絶対パス化したければ:

. "$(cd "$(dirname "$0")" && pwd)/heasoft_env.sh"

Pythonからftoolsを呼ぶ場合も同じことが起きるか?

結論:起きる

  • Pythonがどの環境で起動されたか(Terminal / cron / Jupyter / IDE)で DYLD_LIBRARY_PATH の有無が変わる
  • subprocess(..., shell=True) にすると内部で /bin/sh が起動され、同じ問題が再発しやすい

Python側での堅牢パターン

import os, subprocess

env = os.environ.copy()
if "HEADAS" in env and not env.get("DYLD_LIBRARY_PATH"):
    env["DYLD_LIBRARY_PATH"] = os.path.join(env["HEADAS"], "lib")

subprocess.run(["ftselect", "infile=...", "outfile=...", "expr=..."], env=env, check=True)

まとめ:再現性と保守性のベストプラクティス

  • macOSでは DYLD_* が消える前提で設計する

  • 対話設定(.zshrc)とバッチ設定(スクリプト)は別物

  • 対策は

    1. heasoft_env.sh を1つ作る
    2. 各スクリプトは . "$(dirname "$0")/heasoft_env.sh" を1行だけ追加
  • .sh.py の移行では “呼び出し側”の文字列も一括置換(perl + .bak)が安全

  • Pythonから呼ぶ場合も env を明示するのが堅牢

付録A:最終形(推奨構成)

heasoft_env.sh

case "$(uname -s 2>/dev/null)" in
  Darwin)
    if [ -n "${HEADAS:-}" ] && [ -z "${DYLD_LIBRARY_PATH:-}" ]; then
      export DYLD_LIBRARY_PATH="$HEADAS/lib"
    fi
    ;;
esac

各スクリプトの冒頭(2行目)

#!/bin/sh
. "$(cd "$(dirname "$0")" && pwd)/heasoft_env.sh"

ここは、heasoft_env.sh を自分がおいた場所に依存して変更ください。

(小技) .sh.py に移行したら「参照している側」も一括修正が必要

バッシュで書いていた resolve_util_ftmgtime.sh を、python で書き直して resolve_util_ftmgtime.py としたので、呼んでるファイルの中身を一括で変えたい時とかありますよね。そういう時のための小技を紹介です。

超簡潔・Python一括置換(バックアップ付き)

from pathlib import Path

OLD = "resolve_util_ftmgtime.sh"
NEW = "resolve_util_ftmgtime.py"

for p in Path(".").rglob("*.sh"):
    s = p.read_text()
    if OLD in s:
        p.with_suffix(p.suffix + ".bak").write_text(s)
        p.write_text(s.replace(OLD, NEW))
        print("updated:", p)
実行
python3 replace_ftmgtime.py

これで何をしているか(1行ずつ)

  • Path(".").rglob("*.sh")
    → カレント以下の .sh を全部見る
  • if OLD in s:
    → 該当文字列があるファイルだけ処理
  • .bak を作ってから置換
    安全
  • print
    → 変更したファイルが分かる
置換結果を確認
grep -R --line-number --fixed-strings 'resolve_util_ftmgtime.sh' .

残っていなければOK。

戻したい(バックアップから復旧)

find . -type f -name "*.bak" -print0 | while IFS= read -r -d '' f; do
  mv "$f" "${f%.bak}"
done

付録B:macOS のバージョン依存と dyld サニタイズの適用範囲

B.1 dyld サニタイズは「いつから」問題になる仕様か

本記事で扱っている DYLD サニタイズ(DYLD_ 環境変数の無効化)* は、
ある特定の macOS で突然導入されたものではなく、段階的に強化 されてきた仕様である。

バージョン別の整理

macOS dyld サニタイズの位置づけ
≤ 10.7 (Lion) ~2011 ほぼ無し(Linux的挙動)
10.8 (Mountain Lion) 2012 原型導入(限定的)
10.11 (El Capitan) 2015 SIP導入:本格化
10.13 (High Sierra) 2017 仕様として定着
10.15 (Catalina) 2019 一般ユーザー・研究者にも直撃
11 (Big Sur) 以降 2020– 常時有効と考えてよい

結論
現在運用されている macOS(Big Sur / Monterey / Ventura / Sonoma / Sequoia)では、DYLD_ が「消える前提」で設計しないと、再現性は担保できない* と考えてよい(気がします)。

B.2 「ターミナルでは動く」のに「スクリプトで死ぬ」理由(OS視点)

macOS の dyld サニタイズは、

  • 環境変数の値そのもの
  • どの起動経路でプロセスが生成されたか

組み合わせで発動する。

典型的な違い

起動経路 DYLD_*
対話ターミナル(zsh) 有効なことが多い
bash / sh スクリプト 落とされる場合あり
Python subprocess 落ちやすい
cron / launchd ほぼ常に落ちる
Finder / GUI ほぼ常に落ちる

つまり本記事で示した現象は、

「シェルの違い」ではなく「起動経路の違い」

によるものであり、Linux 的な感覚では説明できない。

付録C:ABI 問題(Intel / Apple Silicon 混在)とは何か

C.1 ABI とは何か(dyld サニタイズとの違い)

ABI(Application Binary Interface) とは、

コンパイル後のバイナリ同士が守るべき“機械語レベルの約束”

である。

具体的には:

  • 関数呼び出し規約
  • レジスタの使い方
  • 構造体のメモリ配置
  • バイナリ形式(Mach-O)
  • CPU アーキテクチャ(x86_64 / arm64)

重要な切り分け

問題 正体
DYLD_* が消える dyld サニタイズ(セキュリティ)
dylib がロードできない(incompatible architecture) ABI 問題

両者は原因も対処法も完全に別である。

C.2 Apple Silicon(M1/M2/M3)で ABI 問題が顕在化した理由

Intel Mac 時代

  • OS:x86_64
  • HEASOFT:x86_64
  • 依存ライブラリ:x86_64

👉 全員同じ ABI --> 問題が見えなかった

Apple Silicon 時代

macOS は 2つの世界を同時に持つ

実体 アーキテクチャ
ネイティブ実行 arm64
互換実行(Rosetta 2) x86_64

この結果、

1つのプロセス内で arm64 と x86_64 は混在できない

という ABI の制約が、実務上のエラーとして現れる。

C.3 HEASOFT でよくある ABI 事故例

典型的な失敗パターン

  • HEASOFT 本体:x86_64(Intel ビルド)
  • Homebrew 依存:arm64
  • 実行環境:Apple Silicon

結果:

dyld: Library not loaded: ...
Reason: incompatible architecture

これは dyld サニタイズではない
export や source では絶対に直らない

C.4 切り分け方法(必須)

# 実行ファイル
file $(which ftselect)

# dylib
file $HEADAS/lib/*.dylib | head

# マシン
uname -m

原則

  • 全部 x86_64 → Rosetta ターミナルで統一
  • 全部 arm64 → ネイティブ環境で統一
  • 混在はしない

C.5 M2 以降は Intel との共存が解消されたのか?

いいえ。解消されていない。

  • M1 / M2 / M3 いずれでも Rosetta 2 は存在
  • Intel バイナリは今も実行可能
  • dyld / ABI の制約は CPU 世代に依存しない

問題の本体は macOS の設計であり、CPU ではない。

付録D:設計指針まとめ(再現性のための要点)

dyld サニタイズ対策

  • DYLD_* は 消える前提
  • 親から継承せず、子で再構成
  • heasoft_env.sh を単一の入口にする

ABI 対策

  • file で必ず確認
  • arm64 / x86_64 を混ぜない
  • Rosetta / ネイティブを意識的に選ぶ

付録の締め

macOS における HEASOFT の不安定動作は、

  • dyld サニタイズ(OSセキュリティ)
  • ABI 問題(CPU/バイナリ仕様)
  • の 2つを切り分けて理解すると、ほぼ理詰めで解決できる(はず)。
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?