Macportsを任意の場所にインストールしたとき、潜在的に遭遇するトラブルを回避する。

- お忙しい方は、[まとめ](#まとめ)をご覧ください。
- (2024/8/13追記) メンテナンスの項(`port selfupdate`でバージョンアップしてしまった際の対応)

## macportsのインストール先を変えた場合に遭遇する問題

macOSのソフトウェアディストリビューションの[Macports](https://www.macports.org/)を使っているが、ディスクのシステム領域を汚さずユーザー領域にインストールして使っている。自分でコンパイルして使うソフトウェアは、`~/Documents/workspace/opr/depot/`以下に、ソフトウェアごとにディレクトリを置いていくことにしているので、[公式サイトのSource Installation](https://www.macports.org/install.php)に従って、
```bash:macportsをインストールした時
% tar xvf MacPorts-2.9.3.tar.bz2 
% cd MacPorts-2.9.3
% ./configure --prefix=${HOME}/Documents/workspace/opr/depot/macports-2.9.3.arm64 \
  --with-no-root-privileges --enable-readline \
  --enable-shared  --without-startupitems \
  --with-applications-dir=${HOME}/Documents/workspace/opr/depot/macports-2.9.3.arm64/Applications \
  --with-frameworks-dir=${HOME}/Documents/workspace/opr/depot/macports-2.9.3.arm64/Library/Frameworks
% make && make install
```
とすれば、問題なく`~/Documents/workspace/opr/depot/macports-2.9.3.arm64/bin/port`が作られ、`~/Documents/workspace/opr/depot/macports-2.9.3.arm64/bin`に`PATH`を通しておけば、普通の使い方、
```bash:portの利用例
% port selfupdate ; port install coreutils
```
で、問題ないソフトは問題なく入れられる。

ところが**ソフトウエアパッケージによっては、[公式サイト](https://ports.macports.org/)では動作確認が取れているのにインストール(コンパイル)に失敗することがある**。しかもビルドログを見ても、単に`File not found!`もしくは、`Can not create file!`的なエラーメッセージだけで何が問題なのかよくわからない、という場面に遭遇した。

また、**全部の`port`がエラーを出すわけではないので、第１感として「Macportsのフレームワークの問題ではなく、個々の`port`の問題」と思ってしまう** ので、なかなか原因特定/解法考案ができなかった。

## 原因
ビルドログを見ていて、`Can not create file: ほにゃらら`というエラーメッセージが出ている。そのため、試しに同じファイル名のファイルを作ってみようとすると、
```bash:同名のファイルの作成を試みる
% touch ほにゃらら
touch: ほにゃらら: File name too long
```
と表示され、ファイル名の長さば許される最大長を超えてしまっていることが原因だと判明した。

macportsは、各ソフトウェアパッケージをコンパイルする際、元の`Portfile`が置かれているディレクトリパス名を使って作業ディレクトリを生成する。

macportsをソースからインストールしたときに`./configure`に与えた`--prefix`オプションの場所(もしくは、設定ファイル`/etc/macports/macports.conf`の中の変数`prefix`)の場所が$prefixだとすると、`Portfile`は、
```bash:Portfileの場所の例
% ${prefix}/var/macports/sources/rsync.macports.org/macports/release/tarballs/ports/lang/llvm-18/Portfile
```
といった場所に置かれる。これをコンパイルするための作業ディレクトリとしては、
```bash:port&ensp;install&ensp;llvm-18の作業ディレクトリ
% ls $macports_root/var/macports/build/
_Users_{username}_Documents_workspace_opr_depot_macports-2.9.3.arm64_var_macports_sources_rsync.macports.org_macports_release_tarballs_ports_lang_llvm-18
```
というように、 **`Portfile`が置かれたディレクトリのフルパスの区切り文字(スラッシュ)をアンダースコアに変えて繋げた非常に長い名前のディレクトリが作られる。** 実際のコンパイル作業は、さらにその下にソースコードが展開されて、コンパイルなどが行われる。 これが、`Filename too long`が起きる主因と思われる。

## macportsに触らずに対処できるか？

この問題は意外と根が深い。まず、作業ディレクトリをどこにするか、というのはmacportsの設定(`${prefix}/etc/macports/macports.conf`)にオプションはない。[`Portfile`を作成するためのマニュアルの変数の説明](https://guide.macports.org/chunked/reference.variables.html)に、`workpath`,`portbuildpath`といった変数が登場するが、**これらの値は変更不能** (`All of these variables except prefix are read-only!`)と明示されている。

macportsを深い階層のディレクトリにおいていることが作業ディレクトリ名を長くしている原因のようなので、たとえば、シンボリックリンクを張って
```bash:回避案(うまくいかない例)
% (cd ~/ ; ln -s ${prefix} macports)
```
```ini:${prefix}/etc/macports/macports.conf(うまくいかない例)
...
prefix                  /Users/{username}/macports
...
```
見かけ上、短いパスにすればいいのではないか、と思い試してみるが、実際にはうまくいかない。というのも、`port`のコードを見てみると、`workpath`は
```bash:portの中身
% nl -ba ${prefix}/bin/port
...
  4079                      # output the path to the port's work directory
  4080                      set workpath [macports::getportworkpath_from_portdir $portdir $portname]
  4081                      if {[file exists $workpath]} {
...  
```
のように定義されているが、生成している関数`macports::getportworkpath_from_portdir`の中では、
```bash:macports.tclの中身
% nl -ba  $prefix/libexec/macports/lib/macports1.0/macports.tcl
...
  2753  proc macports::getportworkpath_from_buildpath {portbuildpath} {
  2754      return [file normalize [file join $portbuildpath work]]
  2755  }
...
```
となっていて、ご丁寧にもシンボリックリンクを実体パスに変換してくれている。(`[file normalize ...]`) 
**どうやらmacportsのコードを触らずに変更するのは難しそうである。**

## 解法案

### 該当コードの特定

上記の`workpath`の定義部分や周囲を見てみると、
```bash:macports.tclの中身
% nl -ba  $prefix/libexec/macports/lib/macports1.0/macports.tcl
...
  3030  
  3031  proc macports::getportbuildpath {id {portname {}}} {
  3032      variable portdbpath
  3033      regsub {://} $id {.} port_path
  3034      regsub -all {/} $port_path {_} port_path
  3035      return [file join $portdbpath build $port_path $portname]
  3036  }
  3037  
  3038  proc macports::getportlogpath {id {portname {}}} {
  3039      variable portdbpath
  3040      regsub {://} $id {.} port_path
  3041      regsub -all {/} $port_path {_} port_path
  3042      return [file join $portdbpath logs $port_path $portname]
  3043  }
  3044  
  .....
  3048  
  3049  proc macports::getportworkpath_from_portdir {portpath {portname {}}} {
  3050      return [macports::getportworkpath_from_buildpath [macports::getportbuildpath $portpath $portname]]
  3051  }
  3052  
```
この辺りが関係していそうである。関数`macports::getportbuildpath`や`macports::getportlogpath`で`Portfile`のパスから作業サブディレクトリ名を生成しているようだ。

### 実装意図の推定

わざわざ`Portfile`から作業ディレクトリ名を生成するのは、察するに、依存関係に従って同時にいくつものソフトウェアパッケージを展開する際にファイルが混じらないようにするためと思われる。また、単純に`portname`としないのは、`${prefix}/etc/macports/macports.conf`のリストにある複数のレポジトリに同じ`portname`のパッケージがある可能性を想定しているのであろう。もしその目的であれば、`port`毎にユニークではない上位ディレクトリ構造`${prefix}/var/macports/sources/`までサブディレクトリ名に含める必要はなさそうである。

さらに、macportsのソースコードをざっくり見渡した感じだと、この文字列変換の逆を行なっているコードはなさそうであるし、そもそも不可逆な変換をしてしまっている。(`${prefix}`のディレクトリ名に`_`が含まれる場合があるので、単射な写像でない。)。 なので、この情報を落としてしまっても他への影響はないのではないかと踏んで、試しに作業ディレクトリ名からユニークではない部分を削って文字列長を短縮する変更をしてmacportsをインストールし直してみた。
(ついでにバージョンアップ)

### コードの修正

変更点としては下記。(`macports`の中では、なんだかいっぱいグローバル変数が使われているようなので、念の為に追加で定義したもの以外の変数の上書き修正は避けた)

```diff:MacPorts-2.10.0-truncate-workpath.patch
diff -rupN MacPorts-2.10.0.orig/src/macports1.0/macports.tcl MacPorts-2.10.0/src/macports1.0/macports.tcl
--- MacPorts-2.10.0.orig/src/macports1.0/macports.tcl	2024-08-07 23:17:43
+++ MacPorts-2.10.0/src/macports1.0/macports.tcl	2024-08-13 20:25:36
@@ -3028,16 +3028,29 @@ proc _source_is_obsolete_svn_repo {source_dir} {
     return 0
 }
 
+proc macports::trim_prefix {fullpath prefix} {
+    set fullpath [file normalize $fullpath]
+    set prefix   [file normalize $prefix]
+    if {[string first $prefix $fullpath] != 0} {
+        return $fullpath
+    }
+    set relative_path [string range $fullpath [string length $prefix] end]
+    set relative_path [string trimleft $relative_path "/\\"]
+    return $relative_path
+}
+
 proc macports::getportbuildpath {id {portname {}}} {
     variable portdbpath
-    regsub {://} $id {.} port_path
+    set s_id [macports::trim_prefix $id [file join $portdbpath sources]]
+    regsub {://} $s_id {.} port_path
     regsub -all {/} $port_path {_} port_path
     return [file join $portdbpath build $port_path $portname]
 }
 
 proc macports::getportlogpath {id {portname {}}} {
     variable portdbpath
-    regsub {://} $id {.} port_path
+    set s_id [macports::trim_prefix $id [file join $portdbpath sources]]
+    regsub {://} $s_id {.} port_path
     regsub -all {/} $port_path {_} port_path
     return [file join $portdbpath logs $port_path $portname]
 }
diff -rupN MacPorts-2.10.0.orig/src/port1.0/tests/portutil.test MacPorts-2.10.0/src/port1.0/tests/portutil.test
--- MacPorts-2.10.0.orig/src/port1.0/tests/portutil.test	2024-08-07 23:17:43
+++ MacPorts-2.10.0/src/port1.0/tests/portutil.test	2024-08-13 20:25:56
@@ -52,9 +52,21 @@ proc init_eval_targets {} {
 
     set mport [mportopen file://.]
 
+    proc trim_prefix {fullpath prefix} {
+        set fullpath [file normalize $fullpath]
+        set prefix   [file normalize $prefix]
+        if {[string first $prefix $fullpath] != 0} {
+            return $fullpath
+        }
+        set relative_path [string range $fullpath [string length $prefix] end]
+        set relative_path [string trimleft $relative_path "/\\"]
+        return $relative_path
+    }
+
     proc getportbuildpath {id {portname ""}} {
         global portdbpath
-        regsub {://} $id {.} port_path
+        set s_id [trim_prefix $id [file join $portdbpath sources]]
+        regsub {://} $s_id {.} port_patha
         regsub -all {/} $port_path {_} port_path
         return [file join $portdbpath build $port_path $portname]
     }
```

### 動作の確認

このパッチを使って下記手順で再インストールを試したところ、

```bash:macportsを修正して再インストール
% tar xvf MacPorts-2.10.0.tar.bz2 
% cd MacPorts-2.10.0
% patch -p1 < ${somewhere}/MacPorts-truncate-workpath.patch
% ./configure --prefix=${HOME}/Documents/workspace/opr/depot/macports-2.10.0.arm64 \
  --with-no-root-privileges --enable-readline \
  --enable-shared  --without-startupitems \
  --with-applications-dir=${HOME}/Documents/workspace/opr/depot/macports-2.10.0.arm64/Applications \
  --with-frameworks-dir=${HOME}/Documents/workspace/opr/depot/macports-2.10.0.arm64/Library/Frameworks
% make && make install
```
とくに問題なくインストールされた。`PATH`を通して試しに
```bash:実行例
% port -v selfupdate
% port -v install coreutils
...
```
とか、実行してみると、`coreutils`は、作業ディレクトリ
```
${prefix}/var/macports/build/rsync.macports.org_macports_release_tarballs_ports_sysutils_coreutils/
```
以下に展開され、サブディレクトリ名は上位ディレクトリを含まない名前に短縮できた。

ソースコード全体を確認してない場当たり対処ですが、いまのところ、とくに副作用はなさそうである。(まだ顕在化してない。)

## メンテナンス
(追記: 2024/8/13)

もし、`port selfupdate`で、macportsのバージョンアップがあると、上記のパッチがあたったものは上書きされてしまうので、その場合には再度書き換える必要がある。

```bash:再パッチ当て
% port -v selfupdate
% (cd ${macports_root}/libexec/macports/lib/macports1.0 ; patch -p3 < {someware}/macports-1.0-buildpath-truncate.patch)
```
などとすればよいはず。

```patch:macports-1.0-buildpath-truncate.patch
diff -rc MacPorts-2.10.0.orig/src/macports1.0/macports.tcl MacPorts-2.10.0/src/macports1.0/macports.tcl
*** MacPorts-2.10.0.orig/src/macports1.0/macports.tcl	Wed Aug  7 23:17:43 2024
--- MacPorts-2.10.0/src/macports1.0/macports.tcl	Tue Aug 13 20:25:36 2024
***************
*** 3028,3043 ****
      return 0
  }
  
  proc macports::getportbuildpath {id {portname {}}} {
      variable portdbpath
!     regsub {://} $id {.} port_path
      regsub -all {/} $port_path {_} port_path
      return [file join $portdbpath build $port_path $portname]
  }
  
  proc macports::getportlogpath {id {portname {}}} {
      variable portdbpath
!     regsub {://} $id {.} port_path
      regsub -all {/} $port_path {_} port_path
      return [file join $portdbpath logs $port_path $portname]
  }
--- 3028,3056 ----
      return 0
  }
  
+ proc macports::trim_prefix {fullpath prefix} {
+     set fullpath [file normalize $fullpath]
+     set prefix   [file normalize $prefix]
+     if {[string first $prefix $fullpath] != 0} {
+         return $fullpath
+     }
+     set relative_path [string range $fullpath [string length $prefix] end]
+     set relative_path [string trimleft $relative_path "/\\"]
+     return $relative_path
+ }
+ 
  proc macports::getportbuildpath {id {portname {}}} {
      variable portdbpath
!     set s_id [macports::trim_prefix $id [file join $portdbpath sources]]
!     regsub {://} $s_id {.} port_path
      regsub -all {/} $port_path {_} port_path
      return [file join $portdbpath build $port_path $portname]
  }
  
  proc macports::getportlogpath {id {portname {}}} {
      variable portdbpath
!     set s_id [macports::trim_prefix $id [file join $portdbpath sources]]
!     regsub {://} $s_id {.} port_path
      regsub -all {/} $port_path {_} port_path
      return [file join $portdbpath logs $port_path $portname]
  }
```


## まとめ

- `Macports`をディレクトリ階層のちょっと深いところにおくと、 ソフトウェアのビルドの際に非常に長い名前の作業ディレクトリを作成し、その下で作業する。そのため **OSが許すファイル名長さの上限を超えてしまうことによるエラーを起こしやすい潜在的な問題がある**. 
(しかも、macportsのインストール先のパスの長さの違いにより起きたり起きなかったりするので、エラーの原因が特定しづらい。)
- 設定ファイルや、シンボリックリンク活用での回避はできない模様。
- そのためソース(Tclのスクリプト)を書き換えることで対処してみた。
  [パッチファイルはここに記述](#コードの修正)。
- 場当たり的対処なので、当然ながら動作未保証。いまのところ顕在化した副作用はない模様。

## 感想

これは`Macports`の問題というよりは、macOSの`FILENAME_MAX`,`PATH_MAX`が1024と小さいことが問題なきがします。(比較として、手元にあるLinux OSだと4096。)