Linux ファームウェアクリーンアップツールの作り方
はじめに
急に「/」配下のディスクの空きがなくなり、キャッシュなどは/homeは別のディスクだったため、どこか空きを作りたくなりました。
ファームウェアディレクトリ/lib/firmwareには膨大なファームウェアファイルが蓄積され、ディスク容量を圧迫していました。しかし、どのファームウェアが実際に使用されているかを手動で判断するのは困難です。
この記事では、使用されていないファームウェアを安全に特定し、バックアップを取りながら削除するPythonツールの実装方法を解説します。
OSに近い部分をいじるのは危険ですが、
誤操作しても致命的な状態にならない設計(フェイルセーフ)と、
そもそも誤操作を防ぐ設計(フェールプルーフ)を組み込むことで
安全なツールにしていく部分を真似べます。
⚠️ 注意:
このツールは「現在の実行環境で観測された情報」に基づいています。
将来使用される可能性のあるファームウェア(USB機器、再起動後、カーネル更新後など)は検出できません。
ツールの概要
このツールは以下の機能を提供します:
- 現在使用中のファームウェアを自動検出
- 不要なファームウェアを特定
- ドライランモードで安全に確認
- バックアップと復元機能
実装のポイント
1. 使用中ファームウェアの検出
lspci/lsusbによるデバイス検出
def get_active_fw_dirs() -> set[str]:
# lspciでデバイスを列挙
lspci_out = run("lspci -k 2>/dev/null")
for keyword, fw_dirs in VENDOR_FW_MAP.items():
if re.search(keyword, lspci_out, re.IGNORECASE):
active.update(fw_dirs)
dmesgによるファームウェアロード履歴
# dmesgでロード済みファームウェアを確認
dmesg_out = run("dmesg 2>/dev/null | grep -i firmware")
fw_pattern = re.compile(r"firmware: [^ ]+ ([a-zA-Z0-9_\-\.]+)(?:/\S+)?")
for m in fw_pattern.finditer(dmesg_out):
subdir_or_file = m.group(1)
if (FIRMWARE_DIR / subdir_or_file).exists():
active.add(subdir_or_file)
この判定はヒューリスティックであり完全ではありません。
2. 安全な削除候補の選定
必須ファイルの保護
ALWAYS_KEEP = {
"updates", # linux-firmware-updatesパッケージ
"regulatory.db", # 無線LAN規制DB
"intel-ucode",
"amd-ucode",
}
GPUベンダー別マッピング
GPU_FW_MAP = {
"amdgpu": ["amdgpu", "radeon"],
"nvidia": ["nvidia"],
"i915": ["i915", "intel"], # Intel GPU
}
3. バックアップと復元機能
安全なファイル移動
def execute_move(candidates: list[Path]) -> dict:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_dir = BACKUP_BASE / timestamp
backup_dir.mkdir(parents=True, exist_ok=True)
# ファイル移動とマニフェスト作成
shutil.move(str(src), str(dst))
自動復元スクリプト生成
def _generate_restore_script(manifest: dict, output_path: Path):
lines = ["#!/bin/bash", "# 自動生成された復元スクリプト"]
lines.append(f'BACKUP_DIR="{manifest["backup_dir"]}"')
# 復元処理
for move in manifest["moves"]:
src = move["dst"]
dst = move["src"]
lines.append(f'mv "{src}" "{dst}" && echo " 復元: {dst}"')
使い方
ドライラン(確認のみ)
sudo python3 firmware_cleanup.py
実際に実行
sudo python3 firmware_cleanup.py --execute
復元
sudo python3 firmware_cleanup.py --restore
実行例
$ sudo python3 firmware_cleanup.py
[INFO] lspci 出力を解析中...
lspci: 'amdgpu' を検出 → ['amdgpu', 'radeon']
lspci(GPU): 'amdgpu' を検出 → ['amdgpu', 'radeon']
[INFO] dmesg を解析中...
dmesg: 'regulatory.db' を検出
Linux Firmware Cleanup レポート
============================================================
モード : 🔍 ドライラン(変更なし)
ファームウェア合計: 1234.5 MB
保持するサイズ : 1000.0 MB
削減できるサイズ: 234.5 MB (19%)
【保持するディレクトリ・ファイル(使用中と判定)】
✅ amdgpu 500.0 MB
✅ radeon 300.0 MB
✅ regulatory.db 1.0 MB (強制保持)
【削除候補(移動対象)】
🗑️ nvidia 200.0 MB
🗑️ intel-ucode 100.0 MB
🗑️ ath10k 50.0 MB
🗑️ rtlwifi 50.0 MB
============================================================
💡 実際に移動するには: sudo python3 firmware_cleanup.py --execute
💡 復元するには : sudo python3 firmware_cleanup.py --restore
まとめ
このツールを作ることで、次の学びがあります。
- 安全に不要なファームウェアを特定
- ディスク容量の最適化(特にルートパーティションの空き確保)
- 自動バックアップによる安全策
- 簡単な復元機能
Linuxシステムのメンテナンスに役立ててください。特に、ルートパーティションの空きが逼迫した際の緊急対応として有効です。
ソースコード全文
Ubuntu22で試して動作確認していますが、自己責任でご利用ください。
#!/usr/bin/env python3
"""
Linux Firmware Cleanup Tool
============================
使われていないファームウェアを特定し、安全に移動(削除)するツール。
デフォルトはドライランモード。元の状態に戻すスクリプトも生成します。
Usage:
sudo python3 firmware_cleanup.py # ①ドライラン(確認のみ)
sudo python3 firmware_cleanup.py --execute # ②実際にファイルを移動
sudo reboot # ③再起動して、動作に問題なければバックアップファイルを削除する
sudo python3 firmware_cleanup.py --restore # ④ ③でWifiなど問題が起きた場合はバックアップから元に戻す
"""
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
from datetime import datetime
from pathlib import Path
# ============================================================
# 設定
# ============================================================
FIRMWARE_DIR = Path("/lib/firmware")
BACKUP_BASE = Path("/var/lib/firmware-backup")
MANIFEST_NAME = "restore_manifest.json"
# システムに必須の可能性が高いディレクトリ・ファイル(削除候補から除外)
ALWAYS_KEEP = {
"updates", # linux-firmware-updatesパッケージ
"regulatory.db", # 無線LAN規制DB
"regulatory.db.p7s",
"intel-ucode",
"amd-ucode",
}
# GPU ベンダー識別キーワード → firmware サブディレクトリのマッピング
GPU_FW_MAP = {
"amdgpu": ["amdgpu", "radeon"],
"nvidia": ["nvidia"],
"i915": ["i915", "intel"], # Intel GPU
"nouveau": ["nvidia"],
}
# PCI/USB 出力で使う正規表現 → firmware サブディレクトリのヒューリスティックマッピング
VENDOR_FW_MAP = {
# Wi-Fi / Bluetooth
"ath": ["ath10k", "ath11k", "ath12k", "qca"],
"broadcom|brcm|bcm[0-9]+": ["brcm", "cypress"],
"rtl": ["rtlwifi", "rtl_bt", "rtw89", "realtek"],
"mt76": ["mediatek"],
"mrvl": ["mrvl", "libertas"],
"rsi": ["rsi"],
"iwl": ["iwlwifi"],
# Audio (Sound Open Firmware, Intel AVS, etc)
"sof": ["intel/sof", "intel/sof-tplg"],
"snd": ["intel/sof", "intel/sof-tplg", "avs"],
# Ethernet / NIC
"bnx2": ["bnx2", "bnx2x"],
"bnxt": ["bnx2x"],
"qed": ["qed"],
"cxgb": ["cxgb4"],
"netronome": ["netronome"],
"mlx": ["mellanox"],
"liquidio": ["liquidio"],
"dpaa": ["dpaa2"],
# その他
"qcom": ["qcom"],
"intel": ["intel", "intel-ucode", "i915"],
"ti": ["ti-connectivity"],
"asihpi": ["asihpi"],
"ueagle": ["ueagle-atm"],
}
# ============================================================
# ユーティリティ
# ============================================================
def run(cmd: str) -> str:
"""コマンドを実行して stdout を返す。失敗しても空文字を返す。"""
try:
result = subprocess.run(
cmd, shell=True, capture_output=True, text=True, timeout=30
)
return result.stdout + result.stderr
except Exception:
return ""
def dir_size_mb(path: Path) -> float:
"""ディレクトリ(またはファイル)のサイズを MB で返す。"""
try:
if not path.exists():
return 0.0
if path.is_file() or path.is_symlink():
return path.stat().st_size / 1024 / 1024
result = subprocess.run(
["du", "-sb", str(path)], capture_output=True, text=True
)
return int(result.stdout.split()[0]) / 1024 / 1024
except Exception:
return 0.0
def fmt_mb(mb: float) -> str:
return f"{mb:.1f} MB"
# ============================================================
# 使用中ファームウェアの検出
# ============================================================
def get_active_fw_dirs() -> set[str]:
"""
現在のシステムで実際に使われている / 必要なファームウェアディレクトリ名を返す。
"""
active: set[str] = set()
# --- 1. lspci でデバイスを列挙 ---
lspci_out = run("lspci -k 2>/dev/null")
print("\n[INFO] lspci 出力を解析中...")
for keyword, fw_dirs in VENDOR_FW_MAP.items():
if re.search(keyword, lspci_out, re.IGNORECASE):
active.update(fw_dirs)
print(f" lspci: '{keyword}' を検出 → {fw_dirs}")
for gpu_kw, fw_dirs in GPU_FW_MAP.items():
if re.search(gpu_kw, lspci_out, re.IGNORECASE):
active.update(fw_dirs)
print(f" lspci(GPU): '{gpu_kw}' を検出 → {fw_dirs}")
# --- 2. lsusb でUSBデバイスを列挙 ---
lsusb_out = run("lsusb 2>/dev/null")
print("[INFO] lsusb 出力を解析中...")
for keyword, fw_dirs in VENDOR_FW_MAP.items():
if re.search(keyword, lsusb_out, re.IGNORECASE):
active.update(fw_dirs)
# --- 3. dmesg でロード済みファームウェアを確認 ---
dmesg_out = run("dmesg 2>/dev/null | grep -i firmware")
print("[INFO] dmesg を解析中...")
# 例: "firmware: direct-loading firmware intel/sof/sof-hda.ri" -> "intel"
# 例: "firmware: direct-loading firmware regulatory.db" -> "regulatory.db"
fw_pattern = re.compile(r"firmware: [^ ]+ ([a-zA-Z0-9_\-\.]+)(?:/\S+)?")
for m in fw_pattern.finditer(dmesg_out):
subdir_or_file = m.group(1)
if (FIRMWARE_DIR / subdir_or_file).exists():
active.add(subdir_or_file)
print(f" dmesg: '{subdir_or_file}' を検出")
# --- 4. 追加の安全策 ---
# CPU マイクロコード
cpuinfo = run("grep -m1 'vendor_id' /proc/cpuinfo")
if "GenuineIntel" in cpuinfo:
active.add("intel-ucode")
print(" CPU: Intel を検出 → intel-ucode を保持")
if "AuthenticAMD" in cpuinfo:
active.add("amd-ucode")
print(" CPU: AMD を検出 → amd-ucode を保持")
# オーディオトポロジーファイル (.tplg) はルートにある場合が多い
for item in FIRMWARE_DIR.glob("*.tplg"):
active.add(item.name)
print(f" Audio: '{item.name}' を自動保護")
return active
# ============================================================
# 削除候補の決定
# ============================================================
def get_candidates(active: set[str]) -> list[Path]:
"""
/lib/firmware 直下のサブディレクトリ・ファイルのうち、
active に含まれないものを削除候補として返す。
"""
candidates = []
# active の中に 'intel/sof' のような階層がある場合、親の 'intel' を保持対象にする
active_roots = set()
for a in active:
active_roots.add(a.split('/')[0])
for item in sorted(FIRMWARE_DIR.iterdir()):
name = item.name
if name in ALWAYS_KEEP:
continue
if name in active_roots:
continue
# 隠しファイルはスキップ
if name.startswith("."):
continue
candidates.append(item)
return candidates
# ============================================================
# レポート表示
# ============================================================
def print_report(active: set[str], candidates: list[Path], dry_run: bool):
total_fw_mb = dir_size_mb(FIRMWARE_DIR)
candidate_mb = sum(dir_size_mb(p) for p in candidates)
keep_mb = total_fw_mb - candidate_mb
print("\n" + "=" * 60)
print(" Linux Firmware Cleanup レポート")
print("=" * 60)
print(f" モード : {'🔍 ドライラン(変更なし)' if dry_run else '⚡ 実行モード'}")
print(f" ファームウェア合計: {fmt_mb(total_fw_mb)}")
print(f" 保持するサイズ : {fmt_mb(keep_mb)}")
print(f" 削減できるサイズ: {fmt_mb(candidate_mb)} ({candidate_mb/total_fw_mb*100:.0f}%)")
print()
# 実際に保持されるルートディレクトリ/ファイルを表示
print("【保持するディレクトリ・ファイル(使用中と判定)】")
active_roots = set(a.split('/')[0] for a in active)
for name in sorted(active_roots):
p = FIRMWARE_DIR / name
if p.exists():
print(f" ✅ {name:<25} {fmt_mb(dir_size_mb(p)):>10}")
for name in sorted(ALWAYS_KEEP):
p = FIRMWARE_DIR / name
if p.exists():
print(f" ✅ {name:<25} {fmt_mb(dir_size_mb(p)):>10} (強制保持)")
print()
print("【削除候補(移動対象)】")
for p in candidates:
print(f" 🗑️ {p.name:<25} {fmt_mb(dir_size_mb(p)):>10}")
print("=" * 60)
# ============================================================
# 実行:ファイルを移動
# ============================================================
def execute_move(candidates: list[Path]) -> dict:
"""
候補ファイル/ディレクトリをバックアップ先に移動し、
復元用マニフェストを返す。
"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_dir = BACKUP_BASE / timestamp
backup_dir.mkdir(parents=True, exist_ok=True)
manifest = {
"timestamp": timestamp,
"backup_dir": str(backup_dir),
"moves": [],
}
moved_count = 0
for src in candidates:
dst = backup_dir / src.name
try:
shutil.move(str(src), str(dst))
manifest["moves"].append({"src": str(src), "dst": str(dst)})
print(f" 移動: {src} → {dst}")
moved_count += 1
except Exception as e:
print(f" [ERROR] {src} の移動に失敗: {e}", file=sys.stderr)
manifest_path = backup_dir / MANIFEST_NAME
manifest_path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False))
print(f"\n✅ {moved_count} 件を移動しました。")
print(f" バックアップ先: {backup_dir}")
print(f" マニフェスト : {manifest_path}")
# 復元スクリプトを生成
restore_script = backup_dir / "restore.sh"
_generate_restore_script(manifest, restore_script)
print(f" 復元スクリプト: {restore_script}")
return manifest
# ============================================================
# 復元スクリプト生成
# ============================================================
def _generate_restore_script(manifest: dict, output_path: Path):
lines = ["#!/bin/bash", "# 自動生成された復元スクリプト", f"# 生成日時: {manifest['timestamp']}", "set -e", ""]
lines.append(f'BACKUP_DIR="{manifest["backup_dir"]}"')
lines.append("")
lines.append('if [ "$(id -u)" -ne 0 ]; then')
lines.append(' echo "このスクリプトは root で実行してください (sudo ./restore.sh)"')
lines.append(" exit 1")
lines.append("fi")
lines.append("")
lines.append('echo "ファームウェアを復元します..."')
for move in manifest["moves"]:
src = move["dst"] # バックアップ先が新しいsrc
dst = move["src"] # 元の場所が新しいdst
dst_parent = str(Path(dst).parent)
lines.append(f'mkdir -p "{dst_parent}"')
# 復元先に既に存在する場合はスキップする bash 処理
lines.append(f'if [ ! -e "{dst}" ]; then')
lines.append(f' mv "{src}" "{dst}" && echo " 復元: {dst}"')
lines.append('else')
lines.append(f' echo " [SKIP] すでに存在します: {dst}"')
lines.append('fi')
lines.append("")
lines.append('echo "復元完了!"')
output_path.write_text("\n".join(lines) + "\n")
output_path.chmod(0o755)
# ============================================================
# 復元モード
# ============================================================
def do_restore():
"""最新のバックアップからファームウェアを復元する。"""
if not BACKUP_BASE.exists():
print("[ERROR] バックアップディレクトリが見つかりません。", file=sys.stderr)
sys.exit(1)
backups = sorted(BACKUP_BASE.iterdir(), reverse=True)
if not backups:
print("[ERROR] バックアップが存在しません。", file=sys.stderr)
sys.exit(1)
latest = backups[0]
manifest_path = latest / MANIFEST_NAME
if not manifest_path.exists():
print(f"[ERROR] マニフェストが見つかりません: {manifest_path}", file=sys.stderr)
sys.exit(1)
manifest = json.loads(manifest_path.read_text())
print(f"バックアップ {manifest['timestamp']} から復元します...")
restored = 0
for move in manifest["moves"]:
src = Path(move["dst"]) # バックアップ場所
dst = Path(move["src"]) # 元の場所
if not src.exists():
print(f" [SKIP] {src} が見つかりません")
continue
if dst.exists():
print(f" [INFO] すでに存在するためスキップ(または手動で削除してください): {dst}")
continue
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(src), str(dst))
print(f" 復元: {dst}")
restored += 1
print(f"\n✅ {restored} 件を復元しました。")
# ============================================================
# メイン
# ============================================================
def main():
global FIRMWARE_DIR
parser = argparse.ArgumentParser(
description="Linux ファームウェア不要ファイル削除ツール(デフォルト: ドライラン)"
)
parser.add_argument(
"--execute",
action="store_true",
help="実際にファイルを移動する(デフォルトはドライラン)",
)
parser.add_argument(
"--restore",
action="store_true",
help="最新のバックアップから元に戻す",
)
parser.add_argument(
"--firmware-dir",
default=str(FIRMWARE_DIR),
help=f"ファームウェアディレクトリ(デフォルト: {FIRMWARE_DIR})",
)
args = parser.parse_args()
FIRMWARE_DIR = Path(args.firmware_dir)
# root チェック(restore / execute 時)
if (args.execute or args.restore) and os.geteuid() != 0:
print("[ERROR] --execute / --restore は root 権限が必要です。sudo で実行してください。")
sys.exit(1)
if args.restore:
do_restore()
return
# 使用中FWを検出
active = get_active_fw_dirs()
# 削除候補を決定
candidates = get_candidates(active)
# レポート表示
dry_run = not args.execute
print_report(active, candidates, dry_run)
if dry_run:
print("\n💡 実際に移動するには: sudo python3 firmware_cleanup.py --execute")
print("💡 復元するには : sudo python3 firmware_cleanup.py --restore")
else:
if not candidates:
print("削除候補はありません。")
return
confirm = input("\n上記のファイルを移動します。続行しますか? [y/N]: ").strip().lower()
if confirm != "y":
print("中止しました。")
sys.exit(0)
execute_move(candidates)
if __name__ == "__main__":
main()