デスクトップアプリなら、ブラウザで使うより単純。そう思って入れた直後に見るべきなのは、アプリの画面ではなく .desktop ファイルです。
8月11日にChatGPTのLinux向けデスクトップアプリが発表された。こういうクライアントが増えると、アプリ本体だけでなく、メニューから何を起動するかを決める定義も手元に増える。Linuxではその役を .desktop が持つ。
今日はテスト用の定義を2つ作って、Exec と Terminal をPythonで見比べた。目で開けば一瞬なのに、更新後の差分まで毎回読むのは続かないんだよね。雑でも同じ基準で警告を出す方が、まずは役に立つ。
何を確認すればいい?
最初に見るのは3点です。
-
Exec: どの実行ファイルに、どんな引数を渡すか -
Terminal: 起動時に端末を開く指定があるか -
MimeType: 独自URLスキームを受け取る登録があるか
ユーザー単位の定義は ~/.local/share/applications、システム全体の定義は /usr/share/applications に置かれることが多いです。まず一覧だけ取るならこれで足ります。
find ~/.local/share/applications /usr/share/applications \
-maxdepth 1 -name '*.desktop' 2>/dev/null | sort
ここで意識したいのは、Exec はシェルスクリプトではない点です。| や ; が見えたからといって、それだけでシェルの演算子になるわけではありません。逆に /bin/sh -c のようにシェルを明示していたら、そこで初めて引数全体を別の目で読む必要が出ます。
この3点は、次の順番で確認すると漏れにくい。
まずは起動定義だけを機械的に見る
次のスクリプトは、完全な仕様検証器ではありません。新しく入れたクライアントや更新差分を読む前の、粗い足切りです。絶対パスで直接バイナリを呼んでいるか、シェルを挟んでいないか、端末起動を要求していないかを調べます。
from __future__ import annotations
import configparser
import os
import shlex
import sys
from pathlib import Path
SHELLS = {"sh", "bash", "zsh", "fish", "dash", "ksh"}
def audit(path: Path) -> list[str]:
parser = configparser.ConfigParser(interpolation=None, strict=False)
parser.optionxform = str
parser.read(path, encoding="utf-8")
if "Desktop Entry" not in parser:
return ["Desktop Entry セクションがない"]
entry = parser["Desktop Entry"]
if entry.get("Type") != "Application":
return [f"Type が Application ではない: {entry.get('Type', '(未指定)')}"]
exec_line = entry.get("Exec", "")
if not exec_line:
return ["Exec が空"]
try:
argv = shlex.split(exec_line)
except ValueError as exc:
return [f"Exec を分解できない: {exc}"]
if not argv:
return ["Exec が空"]
findings: list[str] = []
command = Path(argv[0])
if command.name in SHELLS:
findings.append(f"Exec がシェルを起動する: {argv[0]}")
if not command.is_absolute():
findings.append(f"実行ファイルが絶対パスではない: {argv[0]}")
elif not command.is_file() or not os.access(command, os.X_OK):
findings.append(f"実行できるファイルが見つからない: {command}")
if entry.get("Terminal", "false").lower() == "true":
findings.append("Terminal=true になっている")
return findings
if __name__ == "__main__":
for arg in sys.argv[1:]:
target = Path(arg)
findings = audit(target)
if findings:
print(f"[WARN] {target}")
for finding in findings:
print(f" - {finding}")
else:
print(f"[OK] {target}: Exec は直接実行、Terminal=false")
/usr/bin/printf %U と /bin/sh -c '/tmp/update-client' を入れた2つのテスト用定義で実行しました。前者は通り、後者だけが警告になります。
[OK] /dev/fd/11: Exec は直接実行、Terminal=false
[WARN] /dev/fd/12
- Exec がシェルを起動する: /bin/sh
- Terminal=true になっている
警告が出たら、どこまで掘る?
この出力だけで危険と決める必要はありません。たとえばElectron系のアプリにはラッパーが入り、絶対パスでない Exec が正当な場合もあります。ただ、sh -c、Terminal=true、実体のない絶対パスが同時に出たら、そのまま起動せず配布元のパッケージ内容と更新履歴を確認します。
URLスキームも同じです。MimeType=x-scheme-handler/... があること自体は普通ですが、ブラウザや別アプリから渡されたURLで起動する入口になります。Exec の引数に %u や %U があるなら、そのURLをアプリがどう扱うかまで一度追いたいところです。ここは文字列検査だけでは分かりません。
おわりに
Linuxアプリの導入確認は、署名やハッシュだけでは終わりません。起動経路を一度読むと、更新で何が変わったかも追いやすくなります。
このスクリプトは警告を減らすためのものではなく、読むべきファイルを絞るためのものです。新しいAIクライアントほど、まず .desktop の Exec を差分で見る。この小さい習慣を入れておくと、アプリの便利さと起動時の挙動を分けて判断できます。