tech_note

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

windows_pc_setup_script.md (15058B)


      1 ---
      2 title: Windows PC setupスクリプト
      3 tags: 
      4 - Windows
      5 private: false
      6 updated_at: ''
      7 id: null
      8 organization_url_name: null
      9 slide: false
     10 ---
     11 # 概要
     12 Windowsのセットアップを自動化する方式を検討する記事です.
     13 # 対象読者
     14 - (ライトではない)windowsユーザー
     15 - windowsが`\ちょっとできる`人
     16 # 現行方式
     17 ## 保管方法
     18 
     19 |||
     20 |-|-|
     21 |保管場所|GibHub Private リポジトリ|
     22 |パッケージマネージャー|winget|
     23 
     24 てな感じです.
     25 
     26 ## 複数PC運用について
     27 みなさん,Windows機を複数持ってますよね?(唐突)
     28 それを全て一つのリポジトリにするのもなんだし,でも,2つのPCにしちゃうのもなんだかなぁという感じだと思います.
     29 そこで思いついたのは,`メインPC`は`main`ブランチに,ノートPCは`note`ブランチに分割しちゃう方法です.開発用が在る方は`dev`ブランチとかできて面白いですね.ただし,この運用は,複数に共通する設定の変更が絶望的にやりにくいことです.私はWindows機が2台しかない民なのでこの運用で耐えてますが,流石に3つ4つ出てくると厳しいかもです.
     30 
     31 ## winget
     32 最初はいちいちインストールを回すのがめんどくさくて,wingetのexport/import機能を使ってインストールされているアプリを一致させようとした感じです.ただ,色々と問題があったのでそれについて書きます.
     33 ### 既存パッケージ再インストール問題
     34 最初から標準で入っているパッケージをwingetが再インストールしようとする問題がありました.
     35 時間がかかって非常にだるいので,毎回それが起こるようなパッケージ.jsonをつくって,exportされたjsonから削除するようなpythonスクリプトを書きました.
     36 ```python
     37 import json
     38 import os
     39 from datetime import datetime
     40 
     41 # --- Configuration ---
     42 INPUT_FILE = "winget.json"
     43 REMOVE_LIST_FILE = "wingetRemoveList.json"
     44 OUTPUT_DIR = "winget"
     45 # ---------------------
     46 
     47 # Ensure the output directory exists
     48 if not os.path.exists(OUTPUT_DIR):
     49     os.makedirs(OUTPUT_DIR)
     50     print(f"Created directory: {OUTPUT_DIR}")
     51 
     52 # Read the main winget file
     53 try:
     54     with open(INPUT_FILE, "r", encoding="utf-8") as fs:
     55         wingetJson = json.load(fs)
     56 except FileNotFoundError:
     57     print(f"Error: Input file not found at '{INPUT_FILE}'")
     58     exit(1)
     59 except json.JSONDecodeError:
     60     print(f"Error: Could not decode JSON from '{INPUT_FILE}'")
     61     exit(1)
     62 
     63 # Read the remove list file
     64 try:
     65     with open(REMOVE_LIST_FILE, "r", encoding="utf-8") as fl:
     66         wingetRemoveListJson = json.load(fl)
     67 except FileNotFoundError:
     68     print(f"Error: Remove list file not found at '{REMOVE_LIST_FILE}'")
     69     exit(1)
     70 except json.JSONDecodeError:
     71     print(f"Error: Could not decode JSON from '{REMOVE_LIST_FILE}'")
     72     exit(1)
     73 
     74 remove_identifiers = set(wingetRemoveListJson.get("wingetRemoveList", []))
     75 original_packages = wingetJson.get("Sources", [{}])[0].get("Packages", [])
     76 packages_to_keep = []
     77 removed_count = 0
     78 
     79 for package in original_packages:
     80     if package.get("PackageIdentifier") in remove_identifiers:
     81         print(f"Removing: {package.get('PackageIdentifier')}")
     82         removed_count += 1
     83     else:
     84         packages_to_keep.append(package)
     85 
     86 if removed_count == 0:
     87     print("No packages were removed from the list.")
     88 
     89 wingetJson["Sources"][0]["Packages"] = packages_to_keep
     90 
     91 # Generate the output filename with the current date
     92 today_str = datetime.now().strftime("%Y%m%d")
     93 output_filename = os.path.join(OUTPUT_DIR, f"winget-{today_str}.json")
     94 
     95 # Write the modified data to the new file
     96 with open(output_filename, "w", encoding="utf-8") as f:
     97     json.dump(wingetJson, f, indent=4, ensure_ascii=False)
     98 
     99 print(f"\nSuccessfully created formatted winget file at: {output_filename}")
    100 ```
    101 コードはAIが書いたので,品質は保証しません.私の環境だと上手く動きました.
    102 あと,現時点での`wingetRemoveList.json`の中身も貼っておきます.
    103 ```json
    104 {
    105     "wingetRemoveList" : [
    106         "CPUID.CPU-Z.MSI",
    107         "Microsoft.Office",
    108         "Microsoft.VCRedist.2010.x64",
    109         "Microsoft.VCRedist.2008.x64",
    110         "Ubisoft.Connect",
    111         "Python.Launcher",
    112         "Microsoft.VCRedist.2015+.x64",
    113         "Microsoft.VCRedist.2008.x86",
    114         "Microsoft.VCRedist.2010.x86",
    115         "Microsoft.DotNet.DesktopRuntime.8",
    116         "Microsoft.VCRedist.2015+.x86",
    117         "Python.Python.3.10",
    118         "OffSec.KaliLinux",
    119         "Microsoft.UI.Xaml.2.7",
    120         "Microsoft.UI.Xaml.2.8",
    121         "Microsoft.VCLibs.Desktop.14",
    122         "Microsoft.WSL",
    123         "Microsoft.WindowsTerminal"
    124     ]
    125 }
    126 ```
    127 ## 種々の設定
    128 私はこれら以外にも割と様々にWindowsの設定を施しているので,それらも入れたいなーとか思いました.
    129 具体的に言うと,
    130 - powershellの使い心地をbashに寄せるためのプロファイル適用
    131 - タスクバーの時計に秒数を追加
    132 - 高速スタートアップの無効化
    133 - 視覚効果を無効化
    134 - ダークモード
    135 - システムサウンドの無効化
    136 これらを全部やるようなスクリプトを作っちゃいました.
    137 AIに書かせると楽ですね.この程度なら容易にレビューができるし,ハルシネーションの影響も割合,少なくて済むので有り難いです.
    138 なんか`-Force`が多用されてて怖いですけどね.windowsはもともと品質がよろしくはないので良いかななんて()
    139 `setup.ps1`
    140 ```powershell
    141 <#
    142 .SYNOPSIS
    143     PCのセットアップを自動化します。
    144 .DESCRIPTION
    145     このPowerShellスクリプトは、以下のセットアップ処理を実行します:
    146     - Windowsテーマのダークモードへの変更
    147     - PowerShellプロファイルの更新
    148     - パフォーマンス向上のための視覚効果の無効化
    149     - タスクバー時計への秒表示の有効化
    150     - 高速スタートアップの無効化
    151     - システムサウンドの無効化
    152     - 最新のwinget JSON構成ファイルを使ったアプリケーションのインストール
    153     - shortcutsフォルダのユーザーPATHへの追加
    154 .NOTES
    155     このスクリプトは管理者権限で実行する必要があります。
    156 #>
    157 
    158 # スクリプトが管理者として実行されているか確認
    159 if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
    160     Write-Warning "このスクリプトは管理者権限が必要です。管理者として再起動します..."
    161     # 管理者としてスクリプトを再実行
    162     Start-Process pwsh -Verb RunAs -ArgumentList "-NoProfile -File `"$PSCommandPath`""
    163     exit
    164 }
    165 
    166 # スクリプトの場所を基準に動作するようにカレントディレクトリを変更
    167 $scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
    168 Set-Location -Path $scriptRoot
    169 
    170 Write-Host "PCセットアップスクリプトを開始します..." -ForegroundColor Green
    171 Write-Host "------------------------------------------------------------"
    172 
    173 # 1. Set system and apps to Dark Mode
    174 try {
    175     Write-Host "[1/8] Windowsとアプリのテーマをダークモードに設定しています..." -ForegroundColor Cyan
    176     $regPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"
    177     # 0 = Dark, 1 = Light
    178     Set-ItemProperty -Path $regPath -Name "AppsUseLightTheme" -Value 0 -ErrorAction Stop
    179     Set-ItemProperty -Path $regPath -Name "SystemUsesLightTheme" -Value 0 -ErrorAction Stop
    180     Write-Host "  テーマをダークモードに設定しました。"
    181 }
    182 catch {
    183     Write-Error "ダークモードへの設定に失敗しました: $_"
    184 }
    185 
    186 # 2. PowerShellプロファイルに内容を追記
    187 try {
    188     Write-Host "[2/8] PowerShellプロファイルを更新しています..." -ForegroundColor Cyan
    189     $profileSourcePath = ".\Microsoft.PowerShell_profile.ps1"
    190 
    191     if (-not(Test-Path -Path $profileSourcePath)) {
    192         throw "プロファイルソースファイルが見つかりません: $profileSourcePath"
    193     }
    194 
    195     $profileContent = Get-Content -Path $profileSourcePath -Raw -ErrorAction Stop
    196 
    197     # プロファイル用のディレクトリが存在しない場合は作成
    198     $profileDir = Split-Path -Path $PROFILE -Parent
    199     if (-not (Test-Path -Path $profileDir)) {
    200         New-Item -Path $profileDir -ItemType Directory -Force | Out-Null
    201     }
    202 
    203     # プロファイルファイルが存在しないか、内容がまだ含まれていない場合に追記
    204     if (-not (Test-Path $PROFILE) -or -not ((Get-Content $PROFILE -Raw) -like "*$($profileContent)*")) {
    205          Add-Content -Path $PROFILE -Value $profileContent
    206          Write-Host "  PowerShellプロファイルを更新しました。"
    207     } else {
    208         Write-Host "  プロファイルは既に更新済みです。スキップします。" -ForegroundColor Yellow
    209     }
    210 }
    211 catch {
    212     Write-Error "プロファイルの更新に失敗しました: $_"
    213 }
    214 
    215 # 3. パフォーマンスのために視覚効果を無効化
    216 try {
    217     Write-Host "[3/8] 視覚効果をパフォーマンス優先に設定しています..." -ForegroundColor Cyan
    218     $regPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects"
    219     Set-ItemProperty -Path $regPath -Name "VisualFxSetting" -Value 2 -ErrorAction Stop
    220     Write-Host "  視覚効果を「パフォーマンスを優先する」に設定しました。"
    221 }
    222 catch {
    223     Write-Error "視覚効果の無効化に失敗しました: $_"
    224 }
    225 
    226 # 4. タスクバーの時計に秒を表示
    227 try {
    228     Write-Host "[4/8] タスクバーの時計に秒を表示する設定を有効にしています..." -ForegroundColor Cyan
    229     $regPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
    230     if (-not (Test-Path $regPath)) {
    231         New-Item -Path $regPath -Force | Out-Null
    232     }
    233     Set-ItemProperty -Path $regPath -Name "ShowSecondsInSystemClock" -Value 1 -ErrorAction Stop
    234     Write-Host "  タスクバーの時計に秒が表示されるようになります。"
    235 }
    236 catch {
    237     Write-Error "時計の秒表示設定に失敗しました: $_"
    238 }
    239 
    240 # 5. 高速スタートアップを無効化
    241 try {
    242     Write-Host "[5/8] 高速スタートアップを無効化しています..." -ForegroundColor Cyan
    243     $regPath = "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Power"
    244     Set-ItemProperty -Path $regPath -Name "HiberbootEnabled" -Value 0 -ErrorAction Stop
    245     Write-Host "  高速スタートアップを無効化しました。"
    246 }
    247 catch {
    248     Write-Error "高速スタートアップの無効化に失敗しました: $_"
    249 }
    250 
    251 # 6. システムサウンドを無効化
    252 try {
    253     Write-Host "[6/8] システムサウンドを無効化しています..." -ForegroundColor Cyan
    254     $schemePath = "HKCU:\AppEvents\Schemes"
    255     Set-ItemProperty -Path $schemePath -Name "(Default)" -Value ".None" -ErrorAction Stop
    256     Write-Host "  サウンド設定を「サウンドなし」に変更しました。"
    257 }
    258 catch {
    259     Write-Error "システムサウンドの無効化に失敗しました: $_"
    260 }
    261 
    262 # 7. 最新のwinget JSONファイルからパッケージをインストール
    263 try {
    264     Write-Host "[7/8] Wingetによるアプリケーションのインストールを開始します..." -ForegroundColor Cyan
    265     $latestWingetFile = Get-ChildItem -Path ".\winget" -Filter "winget-*.json" | Sort-Object Name -Descending | Select-Object -First 1
    266     if ($latestWingetFile) {
    267         Write-Host "  最新の構成ファイルが見つかりました: $($latestWingetFile.Name)"
    268         Write-Host "  インポートを開始します。完了まで時間がかかる場合があります..."
    269         winget import -i $latestWingetFile.FullName --accept-package-agreements --accept-source-agreements
    270     } else {
    271         Write-Warning "  'winget-*.json' ファイルが見つかりませんでした。アプリケーションのインストールをスキップします。"
    272     }
    273 }
    274 catch {
    275     Write-Error "Wingetでのアプリケーションインストールに失敗しました: $_"
    276 }
    277 
    278 # 8. shortcutsフォルダをPATH環境変数に追加
    279 try {
    280     Write-Host "[8/8] 'shortcuts' フォルダをPATH環境変数に追加しています..." -ForegroundColor Cyan
    281     $shortcutsPath = Join-Path -Path $scriptRoot -ChildPath "shortcuts"
    282 
    283     if (-not (Test-Path -Path $shortcutsPath)) {
    284          Write-Warning "  'shortcuts' フォルダが見つかりません。スキップします。"
    285     }
    286     else {
    287         $currentUserPath = [System.Environment]::GetEnvironmentVariable("Path", "User")
    288         if ($currentUserPath -notlike "*$shortcutsPath*") {
    289             $newPath = $currentUserPath.TrimEnd(';') + ";" + $shortcutsPath
    290             [System.Environment]::SetEnvironmentVariable("Path", $newPath, "User")
    291             $env:Path = $newPath # 現在のセッションにも反映
    292             Write-Host "  'shortcuts' フォルダをユーザーのPATHに追加しました。"
    293         } else {
    294             Write-Host "  'shortcuts' フォルダは既にユーザーPATHに存在します。スキップします。" -ForegroundColor Yellow
    295         }
    296     }
    297 }
    298 catch {
    299      Write-Error "'shortcuts' フォルダのPATHへの追加に失敗しました: $_"
    300 }
    301 
    302 Write-Host "------------------------------------------------------------"
    303 Write-Host "セットアップスクリプトが完了しました。" -ForegroundColor Green
    304 Write-Host "UI関連の変更を適用するためにExplorerを再起動します。" -ForegroundColor Yellow
    305 Stop-Process -Name explorer -Force
    306 Write-Host "すべての変更を確実に適用するために、コンピューターの再起動を推奨します。" -ForegroundColor Yellow
    307 ```
    308 私は画面が2つある関係でwallpaperの自動設定は諦めました.そもそもは動画をwallpaperにするよう試みたりしてたので,その辺りは今後気が向けばやります.
    309 あとはpowershellのプロファイルの中にインストールが必要なモジュールがあった気がするので,それもこのスクリプトに入れておくと便利かななどと思ってます.
    310 
    311 # まとめ
    312 これでPC初期化は怖くない!
    313 みなさんも1年くらいの周期で初期化していると思うので,この機会にぜひとも自動セットアップを構築して,やってみてください.私は今度は大量のVMのパッケージのバージョンを何とかするansibleを作るかもしれないです.乞うご期待!(のんびりやります)