Merge pull request #1 from FalloutCollaborationProject/update-scripts

Add updating scripts for convenience
This commit is contained in:
Canon
2026-05-22 18:59:05 +08:00
committed by GitHub
8 changed files with 323 additions and 14 deletions
+10 -1
View File
@@ -27,11 +27,14 @@ jobs:
run: dotnet test --configuration Release
- name: Build Windows x64 (self-contained)
run: dotnet publish FCPModUpdater.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o ./publish/win-x64-sc
run: |
dotnet publish FCPModUpdater.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o ./publish/win-x64-sc
cp ./update-fcp-mod-manager.bat ./publish/win-x64-sc/
- name: Build Linux x64 (self-contained)
run: |
dotnet publish FCPModUpdater.csproj -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -o ./publish/linux-x64-sc
cp ./update-fcp-mod-manager.sh ./publish/linux-x64-sc/
cp ./fcp-mod-manager.desktop ./publish/linux-x64-sc/
- name: Build Windows x64 (framework-dependent)
@@ -44,11 +47,14 @@ jobs:
- name: Create archives
run: |
cp ./update-fcp-mod-manager.bat ./publish/
cp ./update-fcp-mod-manager.sh ./publish/
cd ./publish
zip -r "${{ env.APP_NAME }}-win-x64-selfcontained.zip" win-x64-sc
tar -czvf "${{ env.APP_NAME }}-linux-x64-selfcontained.tar.gz" linux-x64-sc
zip -r "${{ env.APP_NAME }}-win-x64.zip" win-x64-fd
tar -czvf "${{ env.APP_NAME }}-linux-x64.tar.gz" linux-x64-fd
sha256sum "${{ env.APP_NAME }}"-*.zip "${{ env.APP_NAME }}"-*.tar.gz > checksums.txt
- name: Create Release
uses: softprops/action-gh-release@v2
@@ -60,3 +66,6 @@ jobs:
./publish/${{ env.APP_NAME }}-linux-x64-selfcontained.tar.gz
./publish/${{ env.APP_NAME }}-win-x64.zip
./publish/${{ env.APP_NAME }}-linux-x64.tar.gz
./publish/checksums.txt
./publish/update-fcp-mod-manager.bat
./publish/update-fcp-mod-manager.sh
+12 -9
View File
@@ -38,6 +38,7 @@ public class UpdateCommand(
if (updateableMods.Count == 0)
{
AnsiConsole.MarkupLine("[green]All FCP mods are up to date![/]");
await RenderAppUpdateNoticeAsync(updateCheckTask);
return 0;
}
@@ -57,15 +58,7 @@ public class UpdateCommand(
ModTableRenderer.RenderUpdateSummary(results);
UpdateCheckResult? updateResult = await updateCheckTask;
if (updateResult != null)
{
AnsiConsole.WriteLine();
var label = updateResult.IsPrerelease ? "Pre-release available" : "Update available";
AnsiConsole.MarkupLine(
$"[yellow bold]{label}: v{updateResult.LatestVersion}[/] [grey](current: {updateResult.CurrentVersion})[/]");
AnsiConsole.MarkupLine($"[grey]Download: {updateResult.ReleaseUrl}[/]");
}
await RenderAppUpdateNoticeAsync(updateCheckTask);
var failCount = results.Count(r => !r.Success);
return failCount > 0 ? 1 : 0;
@@ -81,4 +74,14 @@ public class UpdateCommand(
return 1;
}
}
private static async Task RenderAppUpdateNoticeAsync(Task<UpdateCheckResult?> updateCheckTask)
{
UpdateCheckResult? updateResult = await updateCheckTask;
if (updateResult is null)
return;
AnsiConsole.WriteLine();
AppUpdateNoticeRenderer.Render(updateResult);
}
}
+22
View File
@@ -75,6 +75,28 @@
Downloads are available on the [Releases](https://github.com/FalloutCollaborationProject/FCP-Mod-Updater/releases) page.
### Updating the App
FCP Mod Manager checks for newer app releases and prints update instructions when one is available. The recommended update path is still to download the latest self-contained archive from the [Releases](https://github.com/FalloutCollaborationProject/FCP-Mod-Updater/releases) page and replace your existing app folder.
For convenience, self-contained releases also include manual updater scripts:
| Platform | Script |
|----------|--------|
| Windows | `update-fcp-mod-manager.bat` |
| Linux | `./update-fcp-mod-manager.sh` |
Before running an updater script, close FCP Mod Manager and run the script from the existing app folder. The script will ask whether to use the latest stable release or the latest pre-release, download the matching archive and `checksums.txt`, verify the SHA256 checksum, then replace the files in the current folder.
> [!WARNING]
> The updater scripts are mainly convenience helpers. If an update fails, download the release archive manually and replace the app folder yourself.
> [!WARNING]
> The Windows updater script is less tested than the Linux script. If it fails, use the manual download method from the Releases page.
> [!NOTE]
> The Linux updater requires `curl`, `tar`, and `sha256sum`. Choosing the pre-release channel also requires `python3` for GitHub release JSON parsing.
### Build from Source
```bash
+40
View File
@@ -0,0 +1,40 @@
using System.Runtime.InteropServices;
using FCPModUpdater.Services;
using Spectre.Console;
namespace FCPModUpdater.UI;
public static class AppUpdateNoticeRenderer
{
public static void Render(UpdateCheckResult updateResult)
{
var label = updateResult.IsPrerelease ? "Pre-release available" : "Update available";
var updateCommand = GetUpdateCommand();
var content = new Rows(
new Markup(
$"[yellow bold]{label}: v{Markup.Escape(updateResult.LatestVersion)}[/] [Grey66](current: [/][Grey66 dim bold]{Markup.Escape(updateResult.CurrentVersion)}[/][Grey66])[/]"),
new Markup($"[Grey66]Close this app before updating and run:[/] [underline]{Markup.Escape(updateCommand)}[/]"),
new Markup($"[Grey66]Manual download:[/] [link]{Markup.Escape(updateResult.ReleaseUrl)}[/]"));
var panel = new Panel(content)
{
Border = BoxBorder.Square,
BorderStyle = new Style(Color.Grey, null, Decoration.Dim)
};
panel.Padding = new Padding(1, 0, 1, 0);
panel.Expand = false;
AnsiConsole.Write(panel);
}
private static string GetUpdateCommand()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return @"update-fcp-mod-manager.bat";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return "./update-fcp-mod-manager.sh";
return "download the latest release manually";
}
}
+1 -4
View File
@@ -47,10 +47,7 @@ public class InteractiveMenu
var updateResult = await _updateCheckTask;
if (updateResult != null)
{
var label = updateResult.IsPrerelease ? "Pre-release available" : "Update available";
AnsiConsole.MarkupLine(
$"[yellow bold]{label}: v{updateResult.LatestVersion}[/] [grey](current: {updateResult.CurrentVersion})[/]");
AnsiConsole.MarkupLine($"[grey]Download: {updateResult.ReleaseUrl}[/]");
AppUpdateNoticeRenderer.Render(updateResult);
}
}
+6
View File
@@ -1,9 +1,14 @@
#!/bin/bash
APP_NAME="FCPModUpdater"
mkdir -p ./publish
cp ./update-fcp-mod-manager.bat ./publish/
cp ./update-fcp-mod-manager.sh ./publish/
# Self-contained
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o ./publish/win-x64-sc ./FCPModUpdater.csproj
dotnet publish -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -o ./publish/linux-x64-sc ./FCPModUpdater.csproj
cp ./update-fcp-mod-manager.bat ./publish/win-x64-sc/
cp ./update-fcp-mod-manager.sh ./publish/linux-x64-sc/
cp ./fcp-mod-manager.desktop ./publish/linux-x64-sc/
# Archive self-contained builds
@@ -21,4 +26,5 @@ cp ./fcp-mod-manager.desktop ./publish/linux-x64-fd/
cd ./publish
zip -r "${APP_NAME}-win-x64.zip" win-x64-fd
tar -czvf "${APP_NAME}-linux-x64.tar.gz" linux-x64-fd
sha256sum "${APP_NAME}"-*.zip "${APP_NAME}"-*.tar.gz > checksums.txt
cd ..
+102
View File
@@ -0,0 +1,102 @@
@echo off
setlocal
if /I not "%~1"=="--from-temp" (
set "TEMP_SCRIPT=%TEMP%\update-fcp-mod-manager-%RANDOM%%RANDOM%.bat"
copy "%~f0" "%TEMP_SCRIPT%" >nul
call "%TEMP_SCRIPT%" --from-temp
set "EXIT_CODE=%ERRORLEVEL%"
del "%TEMP_SCRIPT%" >nul 2>nul
exit /b %EXIT_CODE%
)
set "APP_NAME=FCPModUpdater.exe"
set "ARCHIVE=FCPModUpdater-win-x64-selfcontained.zip"
set "BASE_URL=https://github.com/FalloutCollaborationProject/FCP-Mod-Updater/releases/latest/download"
set "RELEASES_API=https://api.github.com/repos/FalloutCollaborationProject/FCP-Mod-Updater/releases?per_page=50"
set "STAGING=%TEMP%\FCPModUpdater-update-%RANDOM%%RANDOM%"
if not exist ".\%APP_NAME%" (
echo Run this script from the existing FCP Mod Manager install folder.
exit /b 1
)
tasklist /FI "IMAGENAME eq %APP_NAME%" 2>nul | find /I "%APP_NAME%" >nul
if "%ERRORLEVEL%"=="0" (
echo FCP Mod Manager is still running. Close it before updating.
exit /b 1
)
mkdir "%STAGING%" >nul 2>nul
if not exist "%STAGING%" (
echo Could not create update staging folder.
exit /b 1
)
echo Select update channel:
echo 1^) Latest stable release
echo 2^) Latest pre-release
set /P "CHANNEL_CHOICE=Choice [1]: "
if "%CHANNEL_CHOICE%"=="" set "CHANNEL_CHOICE=1"
if "%CHANNEL_CHOICE%"=="1" (
echo Downloading latest stable FCP Mod Manager release...
powershell -NoProfile -ExecutionPolicy Bypass -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest -Uri '%BASE_URL%/%ARCHIVE%' -OutFile '%STAGING%\%ARCHIVE%'; Invoke-WebRequest -Uri '%BASE_URL%/checksums.txt' -OutFile '%STAGING%\checksums.txt'"
) else if "%CHANNEL_CHOICE%"=="2" (
echo Finding latest FCP Mod Manager pre-release...
powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue'; $headers = @{ 'User-Agent' = 'FCPModUpdater' }; $releases = Invoke-RestMethod -Headers $headers -Uri '%RELEASES_API%'; $release = $releases | Where-Object { $_.prerelease -and -not $_.draft } | Select-Object -First 1; if ($null -eq $release) { throw 'No pre-release was found.' }; $archive = $release.assets | Where-Object { $_.name -eq '%ARCHIVE%' } | Select-Object -First 1; $checksums = $release.assets | Where-Object { $_.name -eq 'checksums.txt' } | Select-Object -First 1; if ($null -eq $archive) { throw ('No asset named %ARCHIVE% was found on ' + $release.tag_name + '.') }; if ($null -eq $checksums) { throw ('No checksums.txt asset was found on ' + $release.tag_name + '.') }; Invoke-WebRequest -Headers $headers -Uri $archive.browser_download_url -OutFile '%STAGING%\%ARCHIVE%'; Invoke-WebRequest -Headers $headers -Uri $checksums.browser_download_url -OutFile '%STAGING%\checksums.txt'"
) else (
echo Invalid selection.
rmdir /S /Q "%STAGING%" >nul 2>nul
exit /b 1
)
if not "%ERRORLEVEL%"=="0" (
echo Download failed.
rmdir /S /Q "%STAGING%" >nul 2>nul
exit /b 1
)
set "EXPECTED="
for /f "tokens=1" %%A in ('findstr /I /C:" %ARCHIVE%" "%STAGING%\checksums.txt"') do set "EXPECTED=%%A"
if "%EXPECTED%"=="" (
echo Could not find checksum for %ARCHIVE%.
rmdir /S /Q "%STAGING%" >nul 2>nul
exit /b 1
)
set "ACTUAL="
for /f %%A in ('powershell -NoProfile -ExecutionPolicy Bypass -Command "(Get-FileHash -Algorithm SHA256 '%STAGING%\%ARCHIVE%').Hash.ToLowerInvariant()"') do set "ACTUAL=%%A"
if /I not "%ACTUAL%"=="%EXPECTED%" (
echo Checksum verification failed. Update was not installed.
rmdir /S /Q "%STAGING%" >nul 2>nul
exit /b 1
)
powershell -NoProfile -ExecutionPolicy Bypass -Command "Expand-Archive -Force -Path '%STAGING%\%ARCHIVE%' -DestinationPath '%STAGING%'"
if not "%ERRORLEVEL%"=="0" (
echo Could not extract update archive.
rmdir /S /Q "%STAGING%" >nul 2>nul
exit /b 1
)
if not exist "%STAGING%\win-x64-sc" (
echo Downloaded archive did not contain the expected win-x64-sc folder.
rmdir /S /Q "%STAGING%" >nul 2>nul
exit /b 1
)
echo Installing update...
robocopy "%STAGING%\win-x64-sc" "%CD%" /E /COPY:DAT /R:3 /W:1 /NFL /NDL
set "COPY_EXIT=%ERRORLEVEL%"
rmdir /S /Q "%STAGING%" >nul 2>nul
if %COPY_EXIT% GEQ 8 (
echo Update copy failed.
exit /b 1
)
echo Update complete. Start FCP Mod Manager normally.
exit /b 0
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env bash
set -euo pipefail
APP_NAME="FCPModUpdater"
ARCHIVE="FCPModUpdater-linux-x64-selfcontained.tar.gz"
BASE_URL="https://github.com/FalloutCollaborationProject/FCP-Mod-Updater/releases/latest/download"
RELEASES_API="https://api.github.com/repos/FalloutCollaborationProject/FCP-Mod-Updater/releases?per_page=50"
if [[ "${1:-}" != "--from-temp" ]]; then
temp_script="$(mktemp)"
cp "$0" "${temp_script}"
set +e
bash "${temp_script}" --from-temp
exit_code=$?
set -e
rm -f "${temp_script}"
exit "${exit_code}"
fi
if [[ ! -f "./${APP_NAME}" ]]; then
echo "Run this script from the existing FCP Mod Manager install folder."
exit 1
fi
if pgrep -x "${APP_NAME}" >/dev/null 2>&1; then
echo "FCP Mod Manager is still running. Close it before updating."
exit 1
fi
for tool in curl tar sha256sum; do
if ! command -v "${tool}" >/dev/null 2>&1; then
echo "Missing required tool: ${tool}"
exit 1
fi
done
if [[ ! -w "." ]]; then
echo "This install folder is not writable. Move the app to a writable folder or update manually."
exit 1
fi
staging="$(mktemp -d)"
cleanup() {
rm -rf "${staging}"
}
trap cleanup EXIT
echo "Select update channel:"
echo " 1) Latest stable release"
echo " 2) Latest pre-release"
read -r -p "Choice [1]: " channel_choice
case "${channel_choice:-1}" in
1)
echo "Downloading latest stable FCP Mod Manager release..."
curl -fsSL "${BASE_URL}/${ARCHIVE}" -o "${staging}/${ARCHIVE}"
curl -fsSL "${BASE_URL}/checksums.txt" -o "${staging}/checksums.txt"
;;
2)
if ! command -v python3 >/dev/null 2>&1; then
echo "Missing required tool for pre-release selection: python3"
exit 1
fi
echo "Finding latest FCP Mod Manager pre-release..."
curl -fsSL "${RELEASES_API}" -o "${staging}/releases.json"
mapfile -t asset_urls < <(python3 - "${ARCHIVE}" "${staging}/releases.json" <<'PY'
import json
import sys
archive_name = sys.argv[1]
releases_path = sys.argv[2]
with open(releases_path, "r", encoding="utf-8") as handle:
releases = json.load(handle)
release = next((item for item in releases if item.get("prerelease") and not item.get("draft")), None)
if release is None:
sys.exit("No pre-release was found.")
assets = release.get("assets", [])
archive = next((asset for asset in assets if asset.get("name") == archive_name), None)
checksums = next((asset for asset in assets if asset.get("name") == "checksums.txt"), None)
if archive is None:
sys.exit(f"No asset named {archive_name} was found on {release.get('tag_name', 'the pre-release')}.")
if checksums is None:
sys.exit(f"No checksums.txt asset was found on {release.get('tag_name', 'the pre-release')}.")
print(archive["browser_download_url"])
print(checksums["browser_download_url"])
PY
)
if [[ "${#asset_urls[@]}" -ne 2 ]]; then
echo "Could not resolve pre-release download URLs."
exit 1
fi
echo "Downloading latest pre-release FCP Mod Manager release..."
curl -fsSL "${asset_urls[0]}" -o "${staging}/${ARCHIVE}"
curl -fsSL "${asset_urls[1]}" -o "${staging}/checksums.txt"
;;
*)
echo "Invalid selection."
exit 1
;;
esac
cd "${staging}"
if ! grep " ${ARCHIVE}$" checksums.txt | sha256sum -c -; then
echo "Checksum verification failed. Update was not installed."
exit 1
fi
tar -xzf "${ARCHIVE}"
source_dir="${staging}/linux-x64-sc"
if [[ ! -d "${source_dir}" ]]; then
echo "Downloaded archive did not contain the expected linux-x64-sc folder."
exit 1
fi
cd - >/dev/null
echo "Installing update..."
cp -R "${source_dir}/." .
chmod +x "./${APP_NAME}" "./update-fcp-mod-manager.sh"
echo "Update complete. Start FCP Mod Manager normally."