mirror of
https://github.com/xeghosted/OrbisForge.git
synced 2026-10-17 11:57:02 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6859cb8c8 | ||
|
|
6da25b6e69 | ||
|
|
d863dd462d | ||
|
|
abd288c882 | ||
|
|
895858e650 | ||
|
|
53b26f0f98 | ||
|
|
c76d834ace | ||
|
|
88e5214781 | ||
|
|
baea90ea77 |
No files matched your search
@@ -8,6 +8,20 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
unit-tests:
|
||||
name: Unit tests (OrbisForge.Core)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install .NET SDK
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Run tests
|
||||
run: dotnet test tests/OrbisForge.Core.Tests --logger "console;verbosity=normal"
|
||||
|
||||
generate-and-build:
|
||||
name: Generate + build (${{ matrix.name }})
|
||||
runs-on: ubuntu-latest
|
||||
@@ -27,6 +41,10 @@ jobs:
|
||||
type: sprx
|
||||
extra_args: ""
|
||||
artifact: build/ci_sprx.sprx
|
||||
- name: ci_ghplugin
|
||||
type: ghplugin
|
||||
extra_args: ""
|
||||
artifact: build/ci_ghplugin.prx
|
||||
- name: ci_pkg
|
||||
type: exe
|
||||
extra_args: "--pkg"
|
||||
@@ -52,9 +70,19 @@ jobs:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Install OpenOrbis PS4 Toolchain
|
||||
env:
|
||||
# Unauthenticated GitHub API calls are capped at 60/hour PER IP, and GitHub-hosted
|
||||
# runners share IP pools across the whole platform -- that budget gets exhausted by
|
||||
# unrelated workflows constantly. Authenticating with the auto-provided GITHUB_TOKEN
|
||||
# raises it to 5000/hour, scoped to this run.
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -e
|
||||
URL=$(curl -s https://api.github.com/repos/OpenOrbis/OpenOrbis-PS4-Toolchain/releases/latest | grep browser_download_url | cut -d '"' -f4)
|
||||
URL=$(curl -s -H "Authorization: Bearer $GH_TOKEN" https://api.github.com/repos/OpenOrbis/OpenOrbis-PS4-Toolchain/releases/latest | grep browser_download_url | cut -d '"' -f4)
|
||||
if [ -z "$URL" ]; then
|
||||
echo "::error::Could not resolve the OpenOrbis toolchain download URL (GitHub API call returned no browser_download_url)."
|
||||
exit 1
|
||||
fi
|
||||
curl -L -o toolchain.tar.gz "$URL"
|
||||
mkdir -p /tmp/oo-extract
|
||||
tar -xzf toolchain.tar.gz -C /tmp/oo-extract
|
||||
@@ -62,6 +90,19 @@ jobs:
|
||||
echo "OO_PS4_TOOLCHAIN=$HOME/OpenOrbis-PS4-Toolchain" >> "$GITHUB_ENV"
|
||||
echo "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install GoldHEN Plugin SDK
|
||||
if: matrix.type == 'ghplugin'
|
||||
run: |
|
||||
set -e
|
||||
# The SDK's Makefile archives with llvm-ar, which the clang package does
|
||||
# not pull in on the runner image.
|
||||
sudo apt-get install -y llvm
|
||||
git clone --depth 1 https://github.com/GoldHEN/GoldHEN_Plugins_SDK.git /tmp/goldhen-sdk
|
||||
make -C /tmp/goldhen-sdk install
|
||||
# 'make install' ships the headers and libGoldHEN_Hook.a but leaves crtprx.o
|
||||
# behind in the SDK's own build dir; TYPE GHPLUGIN looks for it in lib/.
|
||||
cp /tmp/goldhen-sdk/build/crtprx.o "$OO_PS4_TOOLCHAIN/lib/"
|
||||
|
||||
- name: Build OrbisForge CLI
|
||||
run: dotnet publish src/OrbisForge.Cli -c Release -r linux-x64 --self-contained -p:PublishSingleFile=true -o publish/linux-x64
|
||||
|
||||
|
||||
@@ -3,3 +3,4 @@ obj/
|
||||
publish/
|
||||
.vs/
|
||||
*.user
|
||||
TestResults/
|
||||
@@ -4,6 +4,9 @@
|
||||
<Project Path="src/OrbisForge.Core/OrbisForge.Core.csproj" />
|
||||
<Project Path="src/OrbisForge.Gui/OrbisForge.Gui.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/OrbisForge.Core.Tests/OrbisForge.Core.Tests.csproj" />
|
||||
</Folder>
|
||||
<Project Path="external/UIFramework/src/UIFramework.Controls/UIFramework.Controls.csproj" />
|
||||
<Project Path="external/UIFramework/src/UIFramework.Core/UIFramework.Core.csproj" />
|
||||
</Solution>
|
||||
@@ -9,8 +9,9 @@
|
||||
|
||||
OrbisForge generates self-contained CMake projects for the [OpenOrbis PS4
|
||||
Toolchain](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain), producing `eboot.bin`
|
||||
executables, `.prx` user libraries, `.sprx` signed system libraries, and (optionally) full
|
||||
signed/encrypted `.pkg` installers — through a single `add_orbis_target()` CMake function.
|
||||
executables, `.prx` user libraries, `.sprx` signed system libraries, GoldHEN plugins, and
|
||||
(optionally) full signed/encrypted `.pkg` installers — through a single `add_orbis_target()`
|
||||
CMake function.
|
||||
A Windows GUI wizard and a cross-platform CLI both scaffold the same project layout.
|
||||
|
||||
> The GUI wizard is Windows-only, built on [UIFramework](https://github.com/xeghosted/UIFramework)
|
||||
@@ -19,9 +20,17 @@ A Windows GUI wizard and a cross-platform CLI both scaffold the same project lay
|
||||
|
||||
## Features
|
||||
|
||||
- **All three artifact types** — EXE (`eboot.bin`), PRX, and SPRX, via one `add_orbis_target()`
|
||||
call. `TYPE` selects the right CRT object (`crt1.o` vs `crtlib.o`), the right `create-fself`
|
||||
packaging mode (`--eboot` vs `--lib`), and a sensible default `-ptype`.
|
||||
- **Every artifact type** — EXE (`eboot.bin`), PRX, SPRX, and GHPLUGIN, via one
|
||||
`add_orbis_target()` call. `TYPE` selects the right CRT object (`crt1.o` vs `crtlib.o` vs
|
||||
`crtprx.o`), the right `create-fself` packaging mode (`--eboot` vs `--lib`), and a sensible
|
||||
default `-ptype`.
|
||||
- **GoldHEN plugins** — `TYPE GHPLUGIN` builds a plugin the GoldHEN loader actually accepts.
|
||||
It differs from a plain PRX in three ways that must all be right or the plugin builds
|
||||
cleanly and then silently never loads: the [GoldHEN Plugin
|
||||
SDK](https://github.com/GoldHEN/GoldHEN_Plugins_SDK)'s `crtprx.o` instead of `crtlib.o`,
|
||||
entry at `_init`, and `SceLibcInternal`/`SceSysmodule` instead of `c`/`c++`. Requires the
|
||||
SDK installed into the toolchain — the generated CMake fails with instructions if it isn't.
|
||||
The scaffolded starter plugin shows a PS4 notification from `module_start`.
|
||||
- **Optional full `.pkg` installer** — `add_orbis_package()` stages `eboot.bin` and a generated
|
||||
`param.sfo` into a directory tree matching the final package layout, then drives
|
||||
`create-gp4` + `PkgTool.Core pkg_build` to produce a real signed/encrypted `.pkg`.
|
||||
@@ -34,7 +43,8 @@ A Windows GUI wizard and a cross-platform CLI both scaffold the same project lay
|
||||
or the GUI checkbox. No credentials baked in by design — most PS4 FTP servers run anonymous.
|
||||
- **GUI wizard (Windows)** — pick a name, type, output folder, optional `.pkg` packaging, and
|
||||
optional FTP deploy target; detects your WSL distro and the OpenOrbis toolchain automatically,
|
||||
and surfaces the latest available OpenOrbis release as a reference point.
|
||||
and surfaces the latest available OpenOrbis release as a reference point. Light/dark mode
|
||||
toggle, remembered across launches.
|
||||
- **VS Code integration** — generated projects include a build task (Ctrl+Shift+B) and IntelliSense
|
||||
configuration for the toolchain's unusual target triple.
|
||||
- **CLI scaffolder (Windows + Linux + WSL)** — `orbisforge new`, plus `orbisforge build`/
|
||||
@@ -85,9 +95,21 @@ dotnet publish src/OrbisForge.Cli -c Release -r win-x64 --self-contained -p:Pu
|
||||
dotnet publish src/OrbisForge.Cli -c Release -r linux-x64 --self-contained -p:PublishSingleFile=true -o publish/linux-x64
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```
|
||||
dotnet test tests/OrbisForge.Core.Tests
|
||||
```
|
||||
|
||||
Unit tests cover `OrbisForge.Core`'s scaffolding logic directly (file sets per target type,
|
||||
template content, sanitization, warnings) — fast, no toolchain required. CI's generate-and-build
|
||||
matrix is the complementary end-to-end layer: it proves the *output* actually compiles against
|
||||
the real OpenOrbis toolchain.
|
||||
|
||||
## Project layout
|
||||
|
||||
- `src/OrbisForge.Core` — scaffolding logic (multi-targeted `net8.0`/`netstandard2.0`)
|
||||
- `tests/OrbisForge.Core.Tests` — xUnit tests for Core
|
||||
- `src/OrbisForge.Cli` — cross-platform console scaffolder
|
||||
- `src/OrbisForge.Gui` — Windows wizard (WinForms + UIFramework)
|
||||
- `external/UIFramework` — git submodule
|
||||
|
||||
@@ -5,13 +5,15 @@ var help = """
|
||||
OrbisForge - project scaffolder for the OpenOrbis PS4 Toolchain (CMake-based)
|
||||
|
||||
Usage:
|
||||
orbisforge new --name <name> --type exe|prx|sprx [options]
|
||||
orbisforge new --name <name> --type exe|prx|sprx|ghplugin [options]
|
||||
orbisforge build [path] [--clean]
|
||||
orbisforge deploy [path]
|
||||
|
||||
'new' options:
|
||||
--name <name> Project / target name (required)
|
||||
--type <type> exe, prx, or sprx (required)
|
||||
--type <type> exe, prx, sprx, or ghplugin (required). ghplugin builds a
|
||||
GoldHEN plugin and needs the GoldHEN Plugin SDK installed
|
||||
into the toolchain (crtprx.o + libGoldHEN_Hook.a in lib/).
|
||||
--output <dir> Output directory (default: current directory)
|
||||
--pkg EXE only: also scaffold a full .pkg installer step
|
||||
--ptype <ptype> Override create-fself -ptype (advanced)
|
||||
@@ -33,6 +35,7 @@ var help = """
|
||||
Examples:
|
||||
orbisforge new --name hello_cmake --type exe
|
||||
orbisforge new --name my_plugin --type sprx --output E:\Projects\PS4
|
||||
orbisforge new --name notify_plugin --type ghplugin --output E:\Projects\PS4
|
||||
orbisforge new --name my_game --type exe --pkg
|
||||
orbisforge new --name my_game --type exe --deploy-host 192.168.1.50 --deploy-path /data/homebrew/my_game/
|
||||
orbisforge build
|
||||
@@ -121,7 +124,7 @@ int RunNew(string[] args)
|
||||
|
||||
if (typeText is null || !OrbisTargetTypeExtensions.TryParse(typeText, out var type))
|
||||
{
|
||||
Console.Error.WriteLine("--type is required and must be one of: exe, prx, sprx.");
|
||||
Console.Error.WriteLine("--type is required and must be one of: exe, prx, sprx, ghplugin.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@ public enum OrbisTargetType
|
||||
|
||||
/// <summary>Signed system library/module -> <name>.sprx (ptype "system_dynlib").</summary>
|
||||
Sprx,
|
||||
|
||||
/// <summary>GoldHEN plugin -> <name>.prx (ptype "fake"). Like Prx, but entered at
|
||||
/// _init via the GoldHEN SDK's crtprx.o so the loader reaches module_start.</summary>
|
||||
GoldHenPlugin,
|
||||
}
|
||||
|
||||
public static class OrbisTargetTypeExtensions
|
||||
@@ -23,12 +27,18 @@ public static class OrbisTargetTypeExtensions
|
||||
OrbisTargetType.Exe => "EXE",
|
||||
OrbisTargetType.Prx => "PRX",
|
||||
OrbisTargetType.Sprx => "SPRX",
|
||||
OrbisTargetType.GoldHenPlugin => "GHPLUGIN",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
|
||||
};
|
||||
|
||||
public static bool IsLibrary(this OrbisTargetType type) =>
|
||||
type is OrbisTargetType.Prx or OrbisTargetType.Sprx;
|
||||
|
||||
/// <summary>True when the project's primary source is src/<name>.cpp rather than
|
||||
/// src/main.cpp. Libraries and GoldHEN plugins both have no main().</summary>
|
||||
public static bool UsesNamedSourceFile(this OrbisTargetType type) =>
|
||||
type.IsLibrary() || type is OrbisTargetType.GoldHenPlugin;
|
||||
|
||||
/// <summary>Path to the packaged artifact create-fself produces, relative to the
|
||||
/// project root (i.e. under the CMake build directory).</summary>
|
||||
public static string ArtifactRelativePath(this OrbisTargetType type, string name) => type switch
|
||||
@@ -36,6 +46,7 @@ public static class OrbisTargetTypeExtensions
|
||||
OrbisTargetType.Exe => "build/eboot.bin",
|
||||
OrbisTargetType.Prx => $"build/{name}.prx",
|
||||
OrbisTargetType.Sprx => $"build/{name}.sprx",
|
||||
OrbisTargetType.GoldHenPlugin => $"build/{name}.prx",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
|
||||
};
|
||||
|
||||
@@ -46,6 +57,8 @@ public static class OrbisTargetTypeExtensions
|
||||
case "EXE": case "BIN": type = OrbisTargetType.Exe; return true;
|
||||
case "PRX": type = OrbisTargetType.Prx; return true;
|
||||
case "SPRX": type = OrbisTargetType.Sprx; return true;
|
||||
case "GHPLUGIN": case "GOLDHEN": case "PLUGIN":
|
||||
type = OrbisTargetType.GoldHenPlugin; return true;
|
||||
default: type = default; return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,16 @@ public static class ProjectGenerator
|
||||
WriteFile(Path.Combine(root, "build.bat"), Windows(BuildScriptsTemplate.BuildBat), files);
|
||||
WriteFile(Path.Combine(root, ".vscode", "tasks.json"), VsCodeTemplate.TasksJson, files);
|
||||
WriteFile(Path.Combine(root, ".vscode", "c_cpp_properties.json"), VsCodeTemplate.CCppPropertiesJson, files);
|
||||
WriteFile(Path.Combine(root, ".vscode", "settings.json"), VsCodeTemplate.SettingsJson, files);
|
||||
WriteFile(Path.Combine(root, ".gitignore"), Unix(GitignoreTemplate.Gitignore), files);
|
||||
|
||||
if (options.Type.IsLibrary())
|
||||
if (options.Type == OrbisTargetType.GoldHenPlugin)
|
||||
{
|
||||
// A plugin is entered at module_start, so it gets neither a main() nor
|
||||
// the library pair's public header.
|
||||
WriteFile(Path.Combine(root, "src", $"{name}.cpp"), SourceTemplate.PluginCpp(name), files);
|
||||
}
|
||||
else if (options.Type.IsLibrary())
|
||||
{
|
||||
WriteFile(Path.Combine(root, "src", $"{name}.cpp"), SourceTemplate.LibCpp(name), files);
|
||||
WriteFile(Path.Combine(root, "src", $"{name}.h"), SourceTemplate.LibHeader(name), files);
|
||||
|
||||
@@ -24,19 +24,55 @@ internal static class BuildScriptsTemplate
|
||||
cmake --build build
|
||||
""";
|
||||
|
||||
// Windows entry point: shells into WSL. Deliberately avoids ever referencing an
|
||||
// unquoted $PATH inside the wsl bash -c command line -- WSL auto-injects the full
|
||||
// Windows PATH (with spaces and parentheses like "Program Files (x86)") into that
|
||||
// environment, and passing it through unquoted breaks bash's parser. Instead this
|
||||
// queries OO_PS4_TOOLCHAIN once via its own isolated call and builds an explicit,
|
||||
// clean PATH from it.
|
||||
// Windows entry point. Builds natively when the toolchain is visible from Windows
|
||||
// (its bin\windows tools are then usable), otherwise shells into WSL.
|
||||
//
|
||||
// The WSL path deliberately avoids ever referencing an unquoted $PATH inside the
|
||||
// bash -c command line -- WSL auto-injects the full Windows PATH (with spaces and
|
||||
// parentheses like "Program Files (x86)") into that environment, and passing it
|
||||
// through unquoted breaks bash's parser. Instead this queries OO_PS4_TOOLCHAIN once
|
||||
// via its own isolated call and builds an explicit, clean PATH from it.
|
||||
public const string BuildBat = """
|
||||
@echo off
|
||||
setlocal
|
||||
setlocal EnableDelayedExpansion
|
||||
|
||||
set "SCRIPTDIR=%~dp0"
|
||||
if "%SCRIPTDIR:~-1%"=="\" set "SCRIPTDIR=%SCRIPTDIR:~0,-1%"
|
||||
|
||||
rem vswhere lives under a path containing "(x86)"; resolve it up front, because
|
||||
rem expanding that inside a parenthesised if-block breaks cmd's parser.
|
||||
set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
|
||||
if not defined OO_PS4_TOOLCHAIN goto :wsl
|
||||
|
||||
if /i "%~1"=="clean" (
|
||||
if exist "%SCRIPTDIR%\build" rmdir /s /q "%SCRIPTDIR%\build"
|
||||
echo Cleaned.
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
where cmake >nul 2>&1
|
||||
if errorlevel 1 echo cmake was not found on PATH. & exit /b 1
|
||||
where clang >nul 2>&1
|
||||
if errorlevel 1 echo clang was not found on PATH -- install LLVM and add its bin directory. & exit /b 1
|
||||
|
||||
rem The Visual Studio generators cannot drive this cross-compile, so a
|
||||
rem single-config generator is required. Ninja ships inside VS when it is not
|
||||
rem already on PATH.
|
||||
set "NINJA="
|
||||
for /f "delims=" %%i in ('where ninja 2^>nul') do set "NINJA=%%i"
|
||||
if not defined NINJA if exist "%VSWHERE%" for /f "usebackq delims=" %%i in (`"%VSWHERE%" -latest -products * -property installationPath 2^>nul`) do (
|
||||
if exist "%%i\Common7\IDE\CommonExtensions\Microsoft\CMake\Ninja\ninja.exe" set "NINJA=%%i\Common7\IDE\CommonExtensions\Microsoft\CMake\Ninja\ninja.exe"
|
||||
)
|
||||
if not defined NINJA echo Ninja was not found on PATH or in a Visual Studio install. Try: winget install Ninja-build.Ninja & exit /b 1
|
||||
|
||||
cmake -B "%SCRIPTDIR%\build" -S "%SCRIPTDIR%" -G Ninja -DCMAKE_MAKE_PROGRAM="!NINJA!" -DCMAKE_TOOLCHAIN_FILE="%SCRIPTDIR%/cmake/oo-ps4-toolchain.cmake"
|
||||
if errorlevel 1 exit /b 1
|
||||
cmake --build "%SCRIPTDIR%\build"
|
||||
if errorlevel 1 exit /b 1
|
||||
exit /b 0
|
||||
|
||||
:wsl
|
||||
for /f "usebackq tokens=2 delims==" %%i in (`wsl -- bash -ic "env | grep ^OO_PS4_TOOLCHAIN="`) do set "OO_TOOLCHAIN_WSL=%%i"
|
||||
if "%OO_TOOLCHAIN_WSL%"=="" (
|
||||
echo OO_PS4_TOOLCHAIN is not set in your WSL environment. See the OpenOrbis PS4 Toolchain README.
|
||||
|
||||
@@ -17,7 +17,7 @@ internal static class CMakeListsTemplate
|
||||
sb.AppendLine("endif()");
|
||||
sb.AppendLine();
|
||||
|
||||
var sources = options.Type.IsLibrary()
|
||||
var sources = options.Type.UsesNamedSourceFile()
|
||||
? $"src/{name}.cpp"
|
||||
: "src/main.cpp";
|
||||
|
||||
|
||||
@@ -6,20 +6,34 @@ internal static class OrbisTargetsTemplate
|
||||
# add_orbis_target() / add_orbis_package() - generated by OrbisForge.
|
||||
#
|
||||
# add_orbis_target(<name>
|
||||
# TYPE EXE|PRX|SPRX
|
||||
# TYPE EXE|PRX|SPRX|GHPLUGIN
|
||||
# SOURCES <files...>
|
||||
# [LIBS <names...>] # default: c kernel c++
|
||||
# [PTYPE <ptype>] # default: "fake" (EXE/PRX) or "system_dynlib" (SPRX)
|
||||
# # (GHPLUGIN: SceLibcInternal kernel SceSysmodule GoldHEN_Hook)
|
||||
# [PTYPE <ptype>] # default: "fake", or "system_dynlib" for SPRX
|
||||
# [PAID <hex>] # default: 0x3800000000000011
|
||||
# [LIBNAME <name>] # PRX/SPRX only, passed to create-fself -libname
|
||||
# [LIBNAME <name>] # library types only, passed to create-fself -libname
|
||||
# )
|
||||
#
|
||||
# Links <name> as a PS4 ELF via the OpenOrbis toolchain, then runs
|
||||
# create-fself as a POST_BUILD step to produce the final artifact:
|
||||
# EXE -> eboot.bin
|
||||
# PRX -> <name>.prx
|
||||
# SPRX -> <name>.sprx
|
||||
# EXE -> eboot.bin
|
||||
# PRX -> <name>.prx
|
||||
# SPRX -> <name>.sprx
|
||||
# GHPLUGIN -> <name>.prx
|
||||
#
|
||||
# GHPLUGIN builds a GoldHEN plugin. It differs from PRX in three ways that
|
||||
# all have to be right or the plugin builds cleanly and never loads: it
|
||||
# links the GoldHEN SDK's crtprx.o instead of crtlib.o, enters at _init,
|
||||
# and links SceLibcInternal/SceSysmodule rather than c/c++. Requires the
|
||||
# GoldHEN Plugin SDK installed into the toolchain. Entry points are
|
||||
# module_start/module_stop, which must have C linkage.
|
||||
# readoelf then runs over the packaged .oelf as a build-time sanity check.
|
||||
# A plain, unsigned <name>.elf (copy of that same validated .oelf) is
|
||||
# produced alongside it -- raw-ELF loader tools inspect a file's magic
|
||||
# bytes rather than its extension, so they can't load eboot.bin/*.prx/
|
||||
# *.sprx (which are SELF-wrapped, not plain ELF) no matter what it's
|
||||
# renamed to; point those tools at the .elf instead.
|
||||
|
||||
function(add_orbis_target name)
|
||||
set(options "")
|
||||
@@ -36,12 +50,11 @@ internal static class OrbisTargetsTemplate
|
||||
endif()
|
||||
string(TOUPPER "${ORBIS_TYPE}" ORBIS_TYPE)
|
||||
|
||||
if(NOT ORBIS_LIBS)
|
||||
set(ORBIS_LIBS c kernel c++)
|
||||
endif()
|
||||
|
||||
add_executable(${name} ${ORBIS_SOURCES})
|
||||
|
||||
set(_default_libs c kernel c++)
|
||||
set(_entry_args "")
|
||||
|
||||
if(ORBIS_TYPE STREQUAL "EXE")
|
||||
set(_crt_object "${OO_PS4_TOOLCHAIN}/lib/crt1.o")
|
||||
set(_default_ptype "fake")
|
||||
@@ -57,8 +70,35 @@ internal static class OrbisTargetsTemplate
|
||||
set(_default_ptype "system_dynlib")
|
||||
set(_out_name "${name}.sprx")
|
||||
set(_pkg_flag "--lib")
|
||||
elseif(ORBIS_TYPE STREQUAL "GHPLUGIN")
|
||||
# GoldHEN enters a plugin at _init, which the SDK's crtprx.o forwards
|
||||
# to module_start/module_stop. Built with the stock crtlib.o instead,
|
||||
# a plugin links without complaint and then never loads on console --
|
||||
# so the SDK is checked for here rather than failing silently later.
|
||||
set(_crt_object "${OO_PS4_TOOLCHAIN}/lib/crtprx.o")
|
||||
if(NOT EXISTS "${_crt_object}")
|
||||
message(FATAL_ERROR
|
||||
"add_orbis_target(${name}): TYPE GHPLUGIN needs the GoldHEN Plugin SDK "
|
||||
"installed into the toolchain, but ${_crt_object} is missing. "
|
||||
"Build the SDK (build_static.bat on Windows, make on Linux), then copy "
|
||||
"crtprx.o and libGoldHEN_Hook.a into ${OO_PS4_TOOLCHAIN}/lib and the SDK's "
|
||||
"include/* into ${OO_PS4_TOOLCHAIN}/include/GoldHEN.")
|
||||
endif()
|
||||
set(_default_ptype "fake")
|
||||
set(_out_name "${name}.prx")
|
||||
set(_pkg_flag "--lib")
|
||||
set(_default_libs SceLibcInternal kernel SceSysmodule GoldHEN_Hook)
|
||||
set(_entry_args "LINKER:-e,_init")
|
||||
else()
|
||||
message(FATAL_ERROR "add_orbis_target(${name}): TYPE must be EXE, PRX, or SPRX (got '${ORBIS_TYPE}')")
|
||||
message(FATAL_ERROR "add_orbis_target(${name}): TYPE must be EXE, PRX, SPRX, or GHPLUGIN (got '${ORBIS_TYPE}')")
|
||||
endif()
|
||||
|
||||
if(NOT ORBIS_LIBS)
|
||||
set(ORBIS_LIBS ${_default_libs})
|
||||
endif()
|
||||
|
||||
if(_entry_args)
|
||||
target_link_options(${name} PRIVATE "${_entry_args}")
|
||||
endif()
|
||||
|
||||
# Order matters here and mirrors the toolchain's own sample Makefiles:
|
||||
@@ -83,6 +123,7 @@ internal static class OrbisTargetsTemplate
|
||||
endif()
|
||||
|
||||
set(_out_path "${CMAKE_CURRENT_BINARY_DIR}/${_out_name}")
|
||||
set(_elf_path "${CMAKE_CURRENT_BINARY_DIR}/${name}.elf")
|
||||
|
||||
add_custom_command(TARGET ${name} POST_BUILD
|
||||
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/create-fself"
|
||||
@@ -93,8 +134,9 @@ internal static class OrbisTargetsTemplate
|
||||
--paid ${ORBIS_PAID}
|
||||
${_libname_args}
|
||||
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/readoelf" "$<TARGET_FILE:${name}>.oelf"
|
||||
BYPRODUCTS "${_out_path}"
|
||||
COMMENT "orbis: packaging ${name} -> ${_out_name} (type=${ORBIS_TYPE}, ptype=${_ptype})"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy "$<TARGET_FILE:${name}>.oelf" "${_elf_path}"
|
||||
BYPRODUCTS "${_out_path}" "${_elf_path}"
|
||||
COMMENT "orbis: packaging ${name} -> ${_out_name} + ${name}.elf (type=${ORBIS_TYPE}, ptype=${_ptype})"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
@@ -102,6 +144,7 @@ internal static class OrbisTargetsTemplate
|
||||
ORBIS_OUTPUT_PATH "${_out_path}"
|
||||
ORBIS_OUTPUT_NAME "${_out_name}"
|
||||
ORBIS_TYPE "${ORBIS_TYPE}"
|
||||
ORBIS_ELF_PATH "${_elf_path}"
|
||||
)
|
||||
endfunction()
|
||||
|
||||
|
||||
@@ -41,4 +41,66 @@ internal static class SourceTemplate
|
||||
|
||||
int {{projectName}}_hello(char *buf, size_t buf_size, int num);
|
||||
""";
|
||||
|
||||
public static string PluginCpp(string projectName) => $$"""
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include <GoldHEN/Common.h>
|
||||
|
||||
#define PLUGIN_NAME "{{projectName}}"
|
||||
#define PLUGIN_VERSION "1.00"
|
||||
|
||||
// GoldHEN reads these to list the plugin.
|
||||
#define attr_public __attribute__((visibility("default")))
|
||||
|
||||
attr_public const char *g_pluginName = PLUGIN_NAME;
|
||||
attr_public const char *g_pluginDesc = "Generated by OrbisForge";
|
||||
attr_public const char *g_pluginAuth = "unknown";
|
||||
attr_public const char *g_pluginVersion = PLUGIN_VERSION;
|
||||
|
||||
// This is compiled as C++, so the entry points need C linkage: crtprx.o's
|
||||
// _init resolves module_start/module_stop by their unmangled names.
|
||||
extern "C" {
|
||||
|
||||
static void notify(const char *msg)
|
||||
{
|
||||
OrbisNotificationRequest req;
|
||||
|
||||
memset(&req, 0, sizeof(req));
|
||||
|
||||
req.type = NotificationRequest;
|
||||
req.targetId = -1;
|
||||
req.useIconImageUri = 1;
|
||||
|
||||
strncpy(req.iconUri, "cxml://psnotification/tex_icon_system", sizeof(req.iconUri) - 1);
|
||||
strncpy(req.message, msg, sizeof(req.message) - 1);
|
||||
|
||||
// Non-blocking: module_start must not stall the title's startup.
|
||||
sceKernelSendNotificationRequest(0, &req, sizeof(req), 0);
|
||||
}
|
||||
|
||||
int module_start(size_t argc, const void *argp)
|
||||
{
|
||||
(void)argc;
|
||||
(void)argp;
|
||||
|
||||
notify(PLUGIN_NAME " v" PLUGIN_VERSION " loaded");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int module_stop(size_t argc, const void *argp)
|
||||
{
|
||||
(void)argc;
|
||||
(void)argp;
|
||||
|
||||
notify(PLUGIN_NAME " unloaded");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
""";
|
||||
}
|
||||
@@ -15,7 +15,18 @@ internal static class ToolchainTemplate
|
||||
message(FATAL_ERROR "OO_PS4_TOOLCHAIN environment variable is not set")
|
||||
endif()
|
||||
set(OO_PS4_TOOLCHAIN "$ENV{OO_PS4_TOOLCHAIN}" CACHE PATH "OpenOrbis PS4 toolchain root")
|
||||
set(OO_PS4_TOOLCHAIN_BIN "${OO_PS4_TOOLCHAIN}/bin/linux" CACHE PATH "OpenOrbis PS4 toolchain host tools")
|
||||
|
||||
# create-fself/readoelf are host binaries, so this follows the machine running
|
||||
# the build, not the PS4 target. Overridable via -DOO_PS4_TOOLCHAIN_BIN=... for
|
||||
# the mixed case (e.g. a Windows checkout of the toolchain driven from WSL).
|
||||
if(CMAKE_HOST_WIN32)
|
||||
set(_oo_host_bin "windows")
|
||||
elseif(CMAKE_HOST_APPLE)
|
||||
set(_oo_host_bin "macos")
|
||||
else()
|
||||
set(_oo_host_bin "linux")
|
||||
endif()
|
||||
set(OO_PS4_TOOLCHAIN_BIN "${OO_PS4_TOOLCHAIN}/bin/${_oo_host_bin}" CACHE PATH "OpenOrbis PS4 toolchain host tools")
|
||||
|
||||
set(CMAKE_C_COMPILER clang)
|
||||
set(CMAKE_CXX_COMPILER clang++)
|
||||
|
||||
@@ -54,4 +54,15 @@ internal static class VsCodeTemplate
|
||||
"version": 4
|
||||
}
|
||||
""";
|
||||
|
||||
// The CMakePresets.json we generate is for command-line/WSL cmake use, not for the
|
||||
// CMake Tools extension to drive itself. Outside a WSL/Remote-WSL session it'd pick a
|
||||
// local MSVC kit and try to configure with cl.exe, which can't parse the toolchain
|
||||
// file's clang-style flags (-fPIC, -isysroot, ...) and fails confusingly. Building
|
||||
// always goes through tasks.json (Ctrl+Shift+B) -> build.bat/build.sh instead.
|
||||
public const string SettingsJson = """
|
||||
{
|
||||
"cmake.configureOnOpen": false
|
||||
}
|
||||
""";
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace OrbisForge.Gui
|
||||
{
|
||||
/// <summary>Tiny persisted preference store -- currently just the light/dark choice.
|
||||
/// Not worth a full settings framework for one boolean.</summary>
|
||||
internal static class GuiSettings
|
||||
{
|
||||
private static string FilePath
|
||||
{
|
||||
get
|
||||
{
|
||||
string dir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "OrbisForge");
|
||||
Directory.CreateDirectory(dir);
|
||||
return Path.Combine(dir, "settings.txt");
|
||||
}
|
||||
}
|
||||
|
||||
public static bool LoadDarkMode()
|
||||
{
|
||||
try
|
||||
{
|
||||
return File.Exists(FilePath) && File.ReadAllText(FilePath).Trim() == "dark";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void SaveDarkMode(bool isDark)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(FilePath, isDark ? "dark" : "light");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Not worth surfacing to the user -- worst case the choice doesn't persist.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using UIFramework.Controls;
|
||||
using UIFramework.Core.Controls;
|
||||
using UIFramework.Core.Skinning;
|
||||
using UIFramework.Core.Skinning.Skins;
|
||||
|
||||
using OrbisForge.Core;
|
||||
|
||||
@@ -23,6 +25,7 @@ namespace OrbisForge.Gui
|
||||
private readonly SkinLabel _envLabel = new SkinLabel();
|
||||
private readonly SkinButton _generateButton = new SkinButton();
|
||||
private readonly TextBox _statusBox = new TextBox();
|
||||
private readonly CheckBox _darkModeCheck = new CheckBox();
|
||||
|
||||
private WslDetectionResult _wsl;
|
||||
|
||||
@@ -42,6 +45,20 @@ namespace OrbisForge.Gui
|
||||
const int fieldWidth = 320;
|
||||
int y = 16;
|
||||
|
||||
_darkModeCheck.Text = "Dark mode";
|
||||
_darkModeCheck.AutoSize = true;
|
||||
_darkModeCheck.FlatStyle = FlatStyle.Flat;
|
||||
_darkModeCheck.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||||
_darkModeCheck.Location = new Point(ClientSize.Width - 110, y);
|
||||
_darkModeCheck.Checked = GuiSettings.LoadDarkMode();
|
||||
_darkModeCheck.CheckedChanged += (s, e) =>
|
||||
{
|
||||
ApplyTheme(_darkModeCheck.Checked, save: true);
|
||||
RefreshPlainControlColors();
|
||||
};
|
||||
root.Controls.Add(_darkModeCheck);
|
||||
ApplyTheme(_darkModeCheck.Checked, save: false);
|
||||
|
||||
var nameLabel = new SkinLabel { Text = "Project name:", Location = new Point(left, y), AutoSize = true };
|
||||
y += 20;
|
||||
_nameBox.Location = new Point(left, y);
|
||||
@@ -54,6 +71,7 @@ namespace OrbisForge.Gui
|
||||
_typeBox.Items.Add("EXE (eboot.bin, homebrew app)");
|
||||
_typeBox.Items.Add("PRX (user-mode library)");
|
||||
_typeBox.Items.Add("SPRX (signed system library)");
|
||||
_typeBox.Items.Add("GHPLUGIN (GoldHEN plugin)");
|
||||
_typeBox.SelectedIndex = 0;
|
||||
_typeBox.Location = new Point(left, y);
|
||||
_typeBox.Size = new Size(fieldWidth, 24);
|
||||
@@ -72,11 +90,13 @@ namespace OrbisForge.Gui
|
||||
_pkgCheck.Text = "Also generate full .pkg installer (EXE only)";
|
||||
_pkgCheck.Location = new Point(left, y);
|
||||
_pkgCheck.AutoSize = true;
|
||||
_pkgCheck.FlatStyle = FlatStyle.Flat;
|
||||
y += 28;
|
||||
|
||||
_deployCheck.Text = "Deploy to PS4 via FTP after build";
|
||||
_deployCheck.Location = new Point(left, y);
|
||||
_deployCheck.AutoSize = true;
|
||||
_deployCheck.FlatStyle = FlatStyle.Flat;
|
||||
_deployCheck.CheckedChanged += (s, e) => UpdateDeployFieldsEnabled();
|
||||
y += 24;
|
||||
|
||||
@@ -137,9 +157,37 @@ namespace OrbisForge.Gui
|
||||
|
||||
UpdatePkgCheckEnabled();
|
||||
UpdateDeployFieldsEnabled();
|
||||
RefreshPlainControlColors();
|
||||
Shown += (s, e) => DetectEnvironment();
|
||||
}
|
||||
|
||||
private static void ApplyTheme(bool dark, bool save)
|
||||
{
|
||||
SkinManager.Current = dark ? (ISkin)new DarkSkin() : new LightSkin();
|
||||
if (save) GuiSettings.SaveDarkMode(dark);
|
||||
}
|
||||
|
||||
// _statusBox and the CheckBoxes are plain WinForms controls -- UIFramework has no
|
||||
// skinned equivalents for them (no multiline/scrollable SkinTextBox variant, no
|
||||
// SkinCheckBox at all) -- so they don't auto-repaint on a skin change like every
|
||||
// other control here does. Left at their WinForms defaults, they stay bright white
|
||||
// regardless of the active skin, which looks broken next to a dark-themed form.
|
||||
private void RefreshPlainControlColors()
|
||||
{
|
||||
var textAppearance = SkinManager.Current.GetAppearance(ElementKeys.TextBox, ElementState.Normal);
|
||||
_statusBox.BackColor = textAppearance.Background;
|
||||
_statusBox.ForeColor = textAppearance.ForeColor;
|
||||
|
||||
var panelAppearance = SkinManager.Current.GetAppearance(ElementKeys.Panel, ElementState.Normal);
|
||||
var labelAppearance = SkinManager.Current.GetAppearance(ElementKeys.Label, ElementState.Normal);
|
||||
foreach (var checkBox in new[] { _pkgCheck, _deployCheck, _darkModeCheck })
|
||||
{
|
||||
checkBox.UseVisualStyleBackColor = false;
|
||||
checkBox.BackColor = panelAppearance.Background;
|
||||
checkBox.ForeColor = labelAppearance.ForeColor;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePkgCheckEnabled()
|
||||
{
|
||||
bool isExe = SelectedType() == OrbisTargetType.Exe;
|
||||
@@ -161,6 +209,7 @@ namespace OrbisForge.Gui
|
||||
{
|
||||
case 1: return OrbisTargetType.Prx;
|
||||
case 2: return OrbisTargetType.Sprx;
|
||||
case 3: return OrbisTargetType.GoldHenPlugin;
|
||||
default: return OrbisTargetType.Exe;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\OrbisForge.Core\OrbisForge.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,78 @@
|
||||
using OrbisForge.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace OrbisForge.Core.Tests;
|
||||
|
||||
public class OrbisTargetTypeTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(OrbisTargetType.Exe, "EXE")]
|
||||
[InlineData(OrbisTargetType.Prx, "PRX")]
|
||||
[InlineData(OrbisTargetType.Sprx, "SPRX")]
|
||||
[InlineData(OrbisTargetType.GoldHenPlugin, "GHPLUGIN")]
|
||||
public void ToCMakeString_ReturnsExpectedToken(OrbisTargetType type, string expected)
|
||||
{
|
||||
Assert.Equal(expected, type.ToCMakeString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(OrbisTargetType.Exe, false)]
|
||||
[InlineData(OrbisTargetType.Prx, true)]
|
||||
[InlineData(OrbisTargetType.Sprx, true)]
|
||||
[InlineData(OrbisTargetType.GoldHenPlugin, false)]
|
||||
public void IsLibrary_OnlyTrueForPrxAndSprx(OrbisTargetType type, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, type.IsLibrary());
|
||||
}
|
||||
|
||||
// A plugin isn't a "library" in the lib.cpp + public-header sense, but it still
|
||||
// has no main(), so it shares the src/<name>.cpp layout.
|
||||
[Theory]
|
||||
[InlineData(OrbisTargetType.Exe, false)]
|
||||
[InlineData(OrbisTargetType.Prx, true)]
|
||||
[InlineData(OrbisTargetType.Sprx, true)]
|
||||
[InlineData(OrbisTargetType.GoldHenPlugin, true)]
|
||||
public void UsesNamedSourceFile_TrueForEverythingWithoutMain(OrbisTargetType type, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, type.UsesNamedSourceFile());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("exe", OrbisTargetType.Exe)]
|
||||
[InlineData("EXE", OrbisTargetType.Exe)]
|
||||
[InlineData("bin", OrbisTargetType.Exe)]
|
||||
[InlineData(" prx ", OrbisTargetType.Prx)]
|
||||
[InlineData("SPRX", OrbisTargetType.Sprx)]
|
||||
[InlineData("ghplugin", OrbisTargetType.GoldHenPlugin)]
|
||||
[InlineData("GoldHEN", OrbisTargetType.GoldHenPlugin)]
|
||||
[InlineData(" plugin ", OrbisTargetType.GoldHenPlugin)]
|
||||
public void TryParse_AcceptsKnownAliasesCaseInsensitively(string text, OrbisTargetType expected)
|
||||
{
|
||||
Assert.True(OrbisTargetTypeExtensions.TryParse(text, out var type));
|
||||
Assert.Equal(expected, type);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("elf")]
|
||||
[InlineData("dll")]
|
||||
public void TryParse_RejectsUnknownText(string text)
|
||||
{
|
||||
Assert.False(OrbisTargetTypeExtensions.TryParse(text, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArtifactRelativePath_Exe_IsEbootBin()
|
||||
{
|
||||
Assert.Equal("build/eboot.bin", OrbisTargetType.Exe.ArtifactRelativePath("anything"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(OrbisTargetType.Prx, "my_lib", "build/my_lib.prx")]
|
||||
[InlineData(OrbisTargetType.Sprx, "my_plugin", "build/my_plugin.sprx")]
|
||||
[InlineData(OrbisTargetType.GoldHenPlugin, "notify", "build/notify.prx")]
|
||||
public void ArtifactRelativePath_Libraries_UseNameInFilename(OrbisTargetType type, string name, string expected)
|
||||
{
|
||||
Assert.Equal(expected, type.ArtifactRelativePath(name));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
using System.Text.Json;
|
||||
using OrbisForge.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace OrbisForge.Core.Tests;
|
||||
|
||||
// xUnit creates a fresh instance of this class per test method, so the constructor/Dispose
|
||||
// pair gives each test its own isolated temp directory without any shared-state risk.
|
||||
public class ProjectGeneratorTests : IDisposable
|
||||
{
|
||||
private readonly string _outputDir;
|
||||
|
||||
public ProjectGeneratorTests()
|
||||
{
|
||||
_outputDir = Path.Combine(Path.GetTempPath(), "orbisforge-tests", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(_outputDir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(_outputDir, recursive: true); } catch { /* best effort cleanup */ }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_Exe_WritesMainCppAndCoreFiles()
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "my_exe",
|
||||
Type = OrbisTargetType.Exe,
|
||||
OutputDirectory = _outputDir,
|
||||
});
|
||||
|
||||
Assert.Equal(Path.Combine(_outputDir, "my_exe"), result.RootDirectory);
|
||||
AssertFileExists(result, "src/main.cpp");
|
||||
AssertFileExists(result, "CMakeLists.txt");
|
||||
AssertFileExists(result, "CMakePresets.json");
|
||||
AssertFileExists(result, "cmake/oo-ps4-toolchain.cmake");
|
||||
AssertFileExists(result, "cmake/OrbisTargets.cmake");
|
||||
AssertFileExists(result, "build.bat");
|
||||
AssertFileExists(result, "build.sh");
|
||||
AssertFileExists(result, ".vscode/tasks.json");
|
||||
AssertFileExists(result, ".vscode/c_cpp_properties.json");
|
||||
AssertFileExists(result, ".vscode/settings.json");
|
||||
AssertFileExists(result, ".gitignore");
|
||||
AssertFileExists(result, "README.md");
|
||||
|
||||
AssertFileMissing(result, "src/my_exe.cpp");
|
||||
AssertFileMissing(result, "deploy.bat");
|
||||
AssertFileMissing(result, "sce_sys/icon0.png");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(OrbisTargetType.Prx)]
|
||||
[InlineData(OrbisTargetType.Sprx)]
|
||||
public void Generate_Library_WritesNamedSourceAndHeaderInsteadOfMain(OrbisTargetType type)
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "my_lib",
|
||||
Type = type,
|
||||
OutputDirectory = _outputDir,
|
||||
});
|
||||
|
||||
AssertFileExists(result, "src/my_lib.cpp");
|
||||
AssertFileExists(result, "src/my_lib.h");
|
||||
AssertFileMissing(result, "src/main.cpp");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_GoldHenPlugin_WritesPluginSourceWithoutHeaderOrMain()
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "notify_plugin",
|
||||
Type = OrbisTargetType.GoldHenPlugin,
|
||||
OutputDirectory = _outputDir,
|
||||
});
|
||||
|
||||
AssertFileExists(result, "src/notify_plugin.cpp");
|
||||
AssertFileMissing(result, "src/notify_plugin.h");
|
||||
AssertFileMissing(result, "src/main.cpp");
|
||||
|
||||
var source = ReadFile(result, "src/notify_plugin.cpp");
|
||||
|
||||
// The loader reaches these through crtprx.o's _init, which resolves them by
|
||||
// their unmangled names -- without extern "C" the C++ compiler would mangle
|
||||
// them and the plugin would link but never load.
|
||||
Assert.Contains("extern \"C\"", source);
|
||||
Assert.Contains("int module_start(", source);
|
||||
Assert.Contains("int module_stop(", source);
|
||||
Assert.Contains("sceKernelSendNotificationRequest", source);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(OrbisTargetType.Exe, "TYPE EXE")]
|
||||
[InlineData(OrbisTargetType.Prx, "TYPE PRX")]
|
||||
[InlineData(OrbisTargetType.Sprx, "TYPE SPRX")]
|
||||
[InlineData(OrbisTargetType.GoldHenPlugin, "TYPE GHPLUGIN")]
|
||||
public void Generate_CMakeLists_DeclaresCorrectType(OrbisTargetType type, string expectedSnippet)
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "typed_project",
|
||||
Type = type,
|
||||
OutputDirectory = _outputDir,
|
||||
});
|
||||
|
||||
var content = ReadFile(result, "CMakeLists.txt");
|
||||
Assert.Contains(expectedSnippet, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_WithPtypeOverride_FlowsIntoCMakeLists()
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "custom_ptype",
|
||||
Type = OrbisTargetType.Sprx,
|
||||
OutputDirectory = _outputDir,
|
||||
PType = "secure_module",
|
||||
});
|
||||
|
||||
var content = ReadFile(result, "CMakeLists.txt");
|
||||
Assert.Contains("PTYPE secure_module", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_WithoutPkg_DoesNotAddPackagingBlockOrAssets()
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "no_pkg",
|
||||
Type = OrbisTargetType.Exe,
|
||||
OutputDirectory = _outputDir,
|
||||
IncludePackaging = false,
|
||||
});
|
||||
|
||||
var content = ReadFile(result, "CMakeLists.txt");
|
||||
Assert.DoesNotContain("add_orbis_package", content);
|
||||
AssertFileMissing(result, "sce_sys/icon0.png");
|
||||
Assert.Empty(result.Warnings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_PkgOnPrx_IsIgnoredByGenerator()
|
||||
{
|
||||
// ProjectGenerator only wires packaging for EXE targets even if the flag is set --
|
||||
// the CLI/GUI are expected to reject this combination before calling Generate, but
|
||||
// Generate itself should not blow up or silently produce a broken PRX+pkg mix.
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "prx_with_pkg_flag",
|
||||
Type = OrbisTargetType.Prx,
|
||||
OutputDirectory = _outputDir,
|
||||
IncludePackaging = true,
|
||||
});
|
||||
|
||||
AssertFileMissing(result, "sce_sys/icon0.png");
|
||||
var content = ReadFile(result, "CMakeLists.txt");
|
||||
Assert.DoesNotContain("add_orbis_package", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_PkgWithMissingToolchain_WarnsPerAssetAndStillWritesOtherFiles()
|
||||
{
|
||||
var bogusToolchain = Path.Combine(_outputDir, "does-not-exist");
|
||||
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "pkg_project",
|
||||
Type = OrbisTargetType.Exe,
|
||||
OutputDirectory = _outputDir,
|
||||
IncludePackaging = true,
|
||||
ToolchainPathOverride = bogusToolchain,
|
||||
});
|
||||
|
||||
Assert.Equal(4, result.Warnings.Count); // icon0.png, right.sprx, libSceFios2.prx, libc.prx
|
||||
Assert.All(result.Warnings, w => Assert.Contains("not found, skipped", w));
|
||||
|
||||
var content = ReadFile(result, "CMakeLists.txt");
|
||||
Assert.Contains("add_orbis_package", content);
|
||||
AssertFileExists(result, "src/main.cpp"); // rest of generation still completed
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_WithDeployHost_WritesDeployScriptsWithConfiguredValues()
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "deploy_me",
|
||||
Type = OrbisTargetType.Exe,
|
||||
OutputDirectory = _outputDir,
|
||||
DeployHost = "192.168.1.50",
|
||||
DeployPort = 2121,
|
||||
DeployRemotePath = "/data/homebrew/deploy_me/",
|
||||
});
|
||||
|
||||
AssertFileExists(result, "deploy.bat");
|
||||
AssertFileExists(result, "deploy.sh");
|
||||
|
||||
var bat = ReadFile(result, "deploy.bat");
|
||||
Assert.Contains("ftp://192.168.1.50:2121/data/homebrew/deploy_me/", bat);
|
||||
Assert.Contains(@"build\eboot.bin", bat);
|
||||
|
||||
var sh = ReadFile(result, "deploy.sh");
|
||||
Assert.Contains("ftp://192.168.1.50:2121/data/homebrew/deploy_me/", sh);
|
||||
Assert.Contains("build/eboot.bin", sh);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_WithoutDeployHost_DoesNotWriteDeployScripts()
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "no_deploy",
|
||||
Type = OrbisTargetType.Exe,
|
||||
OutputDirectory = _outputDir,
|
||||
});
|
||||
|
||||
AssertFileMissing(result, "deploy.bat");
|
||||
AssertFileMissing(result, "deploy.sh");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_GitignoreExcludesBuildDirectory()
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "gi_project",
|
||||
Type = OrbisTargetType.Exe,
|
||||
OutputDirectory = _outputDir,
|
||||
});
|
||||
|
||||
var content = ReadFile(result, ".gitignore");
|
||||
Assert.Contains("build/", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_VsCodeFiles_AreValidJson()
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "vscode_project",
|
||||
Type = OrbisTargetType.Exe,
|
||||
OutputDirectory = _outputDir,
|
||||
});
|
||||
|
||||
// JsonDocument.Parse throws on malformed JSON -- that's the assertion.
|
||||
using var tasks = JsonDocument.Parse(ReadFile(result, ".vscode/tasks.json"));
|
||||
using var cpp = JsonDocument.Parse(ReadFile(result, ".vscode/c_cpp_properties.json"));
|
||||
using var settings = JsonDocument.Parse(ReadFile(result, ".vscode/settings.json"));
|
||||
|
||||
Assert.True(tasks.RootElement.TryGetProperty("tasks", out _));
|
||||
Assert.True(cpp.RootElement.TryGetProperty("configurations", out _));
|
||||
Assert.True(settings.RootElement.TryGetProperty("cmake.configureOnOpen", out var configureOnOpen));
|
||||
Assert.False(configureOnOpen.GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_SanitizesNameForDirectoryAndCMakeTarget()
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "my cool game!",
|
||||
Type = OrbisTargetType.Exe,
|
||||
OutputDirectory = _outputDir,
|
||||
});
|
||||
|
||||
Assert.Equal(Path.Combine(_outputDir, "my_cool_game_"), result.RootDirectory);
|
||||
var content = ReadFile(result, "CMakeLists.txt");
|
||||
Assert.Contains("project(my_cool_game_ CXX)", content);
|
||||
// The human-readable name is preserved verbatim elsewhere (e.g. the printf greeting).
|
||||
Assert.Contains("my cool game!", ReadFile(result, "src/main.cpp"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Generate_FilesWrittenList_MatchesActualFilesOnDisk()
|
||||
{
|
||||
var result = ProjectGenerator.Generate(new ProjectOptions
|
||||
{
|
||||
Name = "verify_list",
|
||||
Type = OrbisTargetType.Sprx,
|
||||
OutputDirectory = _outputDir,
|
||||
DeployHost = "10.0.0.1",
|
||||
});
|
||||
|
||||
Assert.NotEmpty(result.FilesWritten);
|
||||
Assert.All(result.FilesWritten, f => Assert.True(File.Exists(f), $"Listed but missing on disk: {f}"));
|
||||
}
|
||||
|
||||
private static void AssertFileExists(GeneratedProject result, string relativePath)
|
||||
{
|
||||
var full = Path.Combine(result.RootDirectory, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
Assert.True(File.Exists(full), $"Expected file not found: {relativePath}");
|
||||
}
|
||||
|
||||
private static void AssertFileMissing(GeneratedProject result, string relativePath)
|
||||
{
|
||||
var full = Path.Combine(result.RootDirectory, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
Assert.False(File.Exists(full), $"Expected file NOT to exist: {relativePath}");
|
||||
}
|
||||
|
||||
private static string ReadFile(GeneratedProject result, string relativePath)
|
||||
{
|
||||
var full = Path.Combine(result.RootDirectory, relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||
return File.ReadAllText(full);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using OrbisForge.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace OrbisForge.Core.Tests;
|
||||
|
||||
public class ProjectOptionsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("my_project", "my_project")]
|
||||
[InlineData("my project", "my_project")]
|
||||
[InlineData(" spaced ", "spaced")]
|
||||
[InlineData("a/b\\c", "a_b_c")]
|
||||
[InlineData("kebab-case_2", "kebab-case_2")]
|
||||
[InlineData("", "orbis_project")]
|
||||
[InlineData(" ", "orbis_project")]
|
||||
[InlineData("!!!", "___")]
|
||||
public void SanitizedName_ReplacesNonAlphanumericSeparators(string input, string expected)
|
||||
{
|
||||
var options = new ProjectOptions { Name = input };
|
||||
Assert.Equal(expected, options.SanitizedName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SanitizedName_PreservesUnicodeLetters()
|
||||
{
|
||||
// char.IsLetterOrDigit is Unicode-aware, so accented letters pass through as-is
|
||||
// rather than getting collapsed to underscores -- documenting that, not just assuming it.
|
||||
var options = new ProjectOptions { Name = "héllo" };
|
||||
Assert.Equal("héllo", options.SanitizedName);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, false)]
|
||||
[InlineData("", false)]
|
||||
[InlineData(" ", false)]
|
||||
[InlineData("192.168.1.50", true)]
|
||||
public void IncludeDeploy_TracksWhetherHostIsSet(string? host, bool expected)
|
||||
{
|
||||
var options = new ProjectOptions { Name = "x", DeployHost = host };
|
||||
Assert.Equal(expected, options.IncludeDeploy);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user