Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 46 additions & 2 deletions New-OsfExfatImage.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
-Label "DATA" `
-ForceOverwrite

2b) Custom temp directory (large copies / slow system drive):
powershell.exe -ExecutionPolicy Bypass -File .\New-OsfExfatImage.ps1 `
-ImagePath "D:\images\data.img" `
-SourceDir "C:\payload" `
-TempDir "D:\temp" `
-ForceOverwrite

3) Create empty image and keep mounted (manual format/copy):
powershell.exe -ExecutionPolicy Bypass -File .\New-OsfExfatImage.ps1 `
-ImagePath "C:\images\data.exfat" `
Expand All @@ -38,6 +45,8 @@
-Size Optional. If omitted, an optimal size is computed to fit all files.
Suffixes: K/M/G/T (1024), k/m/g/t (1000), b (512-byte blocks), or bytes.
-Label Volume label.
-TempDir Optional. Directory for temporary files (format.com logs, etc.).
Aliases: -t. When omitted, Windows default temp is used.
-ForceOverwrite Recreate image if it already exists.
-CreateEmptyAndMount
Create and mount image only. Skip format/copy and leave mounted.
Expand All @@ -64,6 +73,9 @@ param(

[string]$Label = "OSFIMG",

[Alias("t")]
[string]$TempDir,

[switch]$ForceOverwrite,

[switch]$CreateEmptyAndMount
Expand Down Expand Up @@ -123,6 +135,36 @@ function Parse-SizeToBytes([string]$s) {
throw "Failed to parse size string: '$s'"
}

$script:ScriptTempDir = $null

function Initialize-ScriptTempDir([string]$dir) {
if ([string]::IsNullOrWhiteSpace($dir)) { return }
$resolved = $dir.Trim()
if (-not (Test-Path -LiteralPath $resolved -PathType Container)) {
throw "Temp directory not found: $resolved"
}
$resolvedPath = (Resolve-Path -LiteralPath $resolved).Path
$probe = Join-Path $resolvedPath ([System.IO.Path]::GetRandomFileName())
try {
Set-Content -LiteralPath $probe -Value "" -NoNewline
Remove-Item -LiteralPath $probe -Force -ErrorAction SilentlyContinue
} catch {
throw "Temp directory is not writable: $resolvedPath"
}
$script:ScriptTempDir = $resolvedPath
$env:TEMP = $script:ScriptTempDir
$env:TMP = $script:ScriptTempDir
Write-Host "[Info] Temp directory: $script:ScriptTempDir"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

function New-ScriptTempFile {
if ($script:ScriptTempDir) {
$name = [System.IO.Path]::GetRandomFileName()
return (Join-Path $script:ScriptTempDir $name)
}
return [System.IO.Path]::GetTempFileName()
}

function Format-Bytes([Int64]$bytes) {
if ($bytes -ge 1TB) { return "{0:N2} TB" -f ($bytes/1TB) }
if ($bytes -ge 1GB) { return "{0:N2} GB" -f ($bytes/1GB) }
Expand Down Expand Up @@ -255,8 +297,8 @@ function Invoke-FormatVolume([string]$driveLetter, [int]$clusterSize, [string]$l
$lastFormatExitCode = -1
foreach ($attempt in $attempts) {
Write-Host "[Info] format attempt: $($attempt.Name)"
$stdoutPath = [System.IO.Path]::GetTempFileName()
$stderrPath = [System.IO.Path]::GetTempFileName()
$stdoutPath = New-ScriptTempFile
$stderrPath = New-ScriptTempFile
try {
$proc = Start-Process -FilePath "format.com" -ArgumentList $attempt.Args -Wait -PassThru -NoNewWindow `
-RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath
Expand Down Expand Up @@ -295,6 +337,8 @@ if (-not (Test-Admin)) { throw "Please run PowerShell as Administrator." }
if (-not (Test-Path $SourceDir -PathType Container)) { throw "Source directory not found: $SourceDir" }
if (-not (Test-Path (Join-Path $SourceDir "eboot.bin") -PathType Leaf)) { throw "eboot.bin not found in source directory: $SourceDir" }

Initialize-ScriptTempDir -dir $TempDir

# Ensure output directory exists
$outDir = Split-Path -Parent $ImagePath
if ($outDir -and -not (Test-Path $outDir)) {
Expand Down
62 changes: 55 additions & 7 deletions make_image.bat
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,45 @@
setlocal EnableExtensions

REM make_image.bat
REM Usage: make_image.bat "C:\images\data.exfat" "C:\payload"
REM Usage: make_image.bat [-t tmp_dir|--tmp-dir tmp_dir|--temp-dir tmp_dir] "C:\images\data.exfat" "C:\payload"

if "%~1"=="" goto :usage
if "%~2"=="" goto :usage
set "IMAGE="
set "SRCDIR="
set "TEMPDIR="

set "IMAGE=%~1"
set "SRCDIR=%~2"
:parse_args
if "%~1"=="" goto :parse_done
if /i "%~1"=="-t" goto :take_temp
if /i "%~1"=="--tmp-dir" goto :take_temp
if /i "%~1"=="--temp-dir" goto :take_temp
if /i "%~1"=="-h" goto :usage
if /i "%~1"=="--help" goto :usage
if not defined IMAGE (
set "IMAGE=%~1"
shift
goto :parse_args
)
if not defined SRCDIR (
set "SRCDIR=%~1"
shift
goto :parse_args
)
echo [ERROR] Too many arguments.
goto :usage

:take_temp
if "%~2"=="" (
echo [ERROR] Missing value for %~1
goto :usage
)
set "TEMPDIR=%~2"
shift
shift
goto :parse_args

:parse_done
if not defined IMAGE goto :usage
if not defined SRCDIR goto :usage

REM Script is expected to be in the same directory as this BAT
set "SCRIPT=%~dp0New-OsfExfatImage.ps1"
Expand All @@ -29,10 +61,21 @@ if not exist "%SRCDIR%\eboot.bin" (
exit /b 4
)

if defined TEMPDIR (
if not exist "%TEMPDIR%\" (
echo [ERROR] Temp directory not found: "%TEMPDIR%"
exit /b 5
)
)

REM Run elevated? This BAT does not auto-elevate.
REM Right-click -> Run as administrator, or start cmd as admin.

powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT%" -ImagePath "%IMAGE%" -SourceDir "%SRCDIR%" -ForceOverwrite
if defined TEMPDIR (
powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File "%SCRIPT%" -ImagePath "%IMAGE%" -SourceDir "%SRCDIR%" -TempDir "%TEMPDIR%" -ForceOverwrite
) else (
powershell.exe -NoProfile -ExecutionPolicy RemoteSigned -File "%SCRIPT%" -ImagePath "%IMAGE%" -SourceDir "%SRCDIR%" -ForceOverwrite
)

set "RC=%ERRORLEVEL%"
if not "%RC%"=="0" (
Expand All @@ -45,7 +88,12 @@ exit /b 0

:usage
echo Usage:
echo %~nx0 "C:\path\to\image.img" "C:\path\to\folder"
echo %~nx0 [-t tmp_dir ^| --tmp-dir tmp_dir ^| --temp-dir tmp_dir] "C:\path\to\image.img" "C:\path\to\folder"
echo.
echo Examples:
echo %~nx0 "D:\out\game.exfat" "C:\payload"
echo %~nx0 -t D:\temp "D:\out\game.exfat" "C:\payload"
echo %~nx0 "D:\out\game.exfat" "C:\payload" --temp-dir D:\temp
echo.
echo Notes:
echo - Run this BAT as Administrator.
Expand Down
98 changes: 89 additions & 9 deletions mkexfat.sh
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,17 +1,75 @@
#!/bin/sh
# For WSL2/Ubuntu/Debian: sudo apt-get install -y exfatprogs exfat-fuse fuse3 rsync
# Create an exFAT image from a directory
# Usage: mkexfat.sh <input_dir> [output_file]
# Usage: mkexfat.sh [-t tmp_dir|--tmp-dir tmp_dir] <input_dir> [output_file]

set -e

if [ -z "$1" ]; then
echo "Usage: $0 <input_dir> [output_file]"
TMP_BASE="/tmp"
INPUT_DIR=""
OUTPUT=""

usage() {
echo "Usage: $0 [-t tmp_dir|--tmp-dir tmp_dir|--temp-dir tmp_dir] <input_dir> [output_file]"
}

while [ $# -gt 0 ]; do
case "$1" in
-t|--tmp-dir|--temp-dir)
if [ -z "$2" ]; then
echo "Error: missing value for $1"
usage
exit 1
fi
TMP_BASE="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
--)
shift
while [ $# -gt 0 ]; do
if [ -z "$INPUT_DIR" ]; then
INPUT_DIR="$1"
elif [ -z "$OUTPUT" ]; then
OUTPUT="$1"
else
echo "Error: too many positional arguments"
usage
exit 1
fi
shift
done
break
;;
-*)
echo "Error: unknown option: $1"
usage
exit 1
;;
*)
if [ -z "$INPUT_DIR" ]; then
INPUT_DIR="$1"
elif [ -z "$OUTPUT" ]; then
OUTPUT="$1"
else
echo "Error: too many positional arguments"
usage
exit 1
fi
shift
;;
esac
done

if [ -z "$INPUT_DIR" ]; then
usage
exit 1
fi

INPUT_DIR="$1"
OUTPUT="${2:-test.exfat}"
OUTPUT="${OUTPUT:-test.exfat}"

if [ ! -d "$INPUT_DIR" ]; then
echo "Error: input directory not found: $INPUT_DIR"
Expand All @@ -23,6 +81,15 @@ if [ ! -f "$INPUT_DIR/eboot.bin" ]; then
exit 1
fi

if [ ! -d "$TMP_BASE" ]; then
echo "Error: temp directory not found: $TMP_BASE"
exit 1
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if [ ! -w "$TMP_BASE" ] || [ ! -x "$TMP_BASE" ]; then
echo "Error: temp directory is not writable/executable: $TMP_BASE"
exit 1
fi

# More accurate sizing for exFAT:
# - file payload rounded to cluster size
# - FAT + allocation bitmap estimates from cluster count
Expand Down Expand Up @@ -84,13 +151,26 @@ echo "Input size (exFAT alloc): $DATA_BYTES bytes"
echo "Files: $FILE_COUNT, Dirs: $DIR_COUNT"
echo "exFAT profile: -c $MKFS_CLUSTER_ARG (avg file=$AVG_FILE_BYTES bytes)"
echo "Image size: ${MB}MB"
echo "Temp directory: $TMP_BASE"

truncate -s "${MB}M" "$OUTPUT"
mkfs.exfat -c "$MKFS_CLUSTER_ARG" "$OUTPUT"
mkdir -p /mnt/exfat
mount -t exfat-fuse -o loop "$OUTPUT" /mnt/exfat
rsync -r --info=progress2 "$INPUT_DIR"/ /mnt/exfat/
MOUNT_DIR=$(mktemp -d "$TMP_BASE/mkexfat.XXXXXX")

cleanup() {
if mountpoint -q "$MOUNT_DIR" 2>/dev/null; then
umount "$MOUNT_DIR" || true
fi
rmdir "$MOUNT_DIR" 2>/dev/null || true
}

umount /mnt/exfat
trap cleanup EXIT INT TERM

# exfat-fuse can open a plain file without a kernel loop device. mount -o loop
# uses libmount/losetup and can fail on some removable exFAT targets (~large images).
if ! mount -t exfat-fuse "$OUTPUT" "$MOUNT_DIR"; then
mount -t exfat-fuse -o loop "$OUTPUT" "$MOUNT_DIR"
fi
rsync -r --info=progress2 "$INPUT_DIR"/ "$MOUNT_DIR"/

echo "Created $OUTPUT"
Loading