mirror of
https://github.com/xeghosted/OrbisForge.git
synced 2026-10-06 09:00:41 +02:00
Initial commit: OrbisForge CMake project scaffolder for OpenOrbis PS4 Toolchain
This commit is contained in:
commit
0699dcb2e4
24 files changed
+1339
No files matched your search
@@ -0,0 +1,5 @@
|
||||
bin/
|
||||
obj/
|
||||
publish/
|
||||
.vs/
|
||||
*.user
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "external/UIFramework"]
|
||||
path = external/UIFramework
|
||||
url = https://github.com/xeghosted/UIFramework.git
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 xeghosted
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,9 @@
|
||||
<Solution>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/OrbisForge.Cli/OrbisForge.Cli.csproj" />
|
||||
<Project Path="src/OrbisForge.Core/OrbisForge.Core.csproj" />
|
||||
<Project Path="src/OrbisForge.Gui/OrbisForge.Gui.csproj" />
|
||||
</Folder>
|
||||
<Project Path="external/UIFramework/src/UIFramework.Controls/UIFramework.Controls.csproj" />
|
||||
<Project Path="external/UIFramework/src/UIFramework.Core/UIFramework.Core.csproj" />
|
||||
</Solution>
|
||||
@@ -0,0 +1,77 @@
|
||||
# OrbisForge
|
||||
|
||||
**Scaffold and build OpenOrbis PS4 homebrew projects with CMake — on Windows or Linux/WSL.**
|
||||
|
||||
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.
|
||||
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)
|
||||
> (skinned WinForms). The CLI (`orbisforge`) runs natively on Windows, Linux, and WSL, published
|
||||
> self-contained so no .NET install is required to use it.
|
||||
|
||||
## 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`.
|
||||
- **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`.
|
||||
- **Cross-platform builds** — generated projects carry their own `cmake/` core (toolchain file +
|
||||
target helpers), a `build.bat` that shells into WSL, and a `build.sh` for native Linux/WSL.
|
||||
- **Automatic sanity check** — every packaged target runs `readoelf` over its own output as a
|
||||
post-build step, catching malformed builds before you ever copy them to hardware.
|
||||
- **GUI wizard (Windows)** — pick a name, type, output folder, and optional `.pkg` packaging;
|
||||
detects your WSL distro and the OpenOrbis toolchain automatically.
|
||||
- **CLI scaffolder (Windows + Linux + WSL)** — `orbisforge new --name foo --type sprx`.
|
||||
|
||||
## Requirements
|
||||
|
||||
- The [OpenOrbis PS4 Toolchain](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain) installed,
|
||||
with `OO_PS4_TOOLCHAIN` set (in WSL, if building from Windows).
|
||||
- clang, lld, and CMake ≥ 3.20 available wherever the actual build runs (WSL/Linux).
|
||||
- Windows GUI: .NET Framework 4.8 (ships with Windows 10/11).
|
||||
- CLI: nothing — the published binaries are self-contained.
|
||||
|
||||
## Using the CLI
|
||||
|
||||
```
|
||||
orbisforge new --name my_plugin --type sprx --output E:\Projects\PS4
|
||||
orbisforge new --name my_game --type exe --pkg
|
||||
```
|
||||
|
||||
Run `orbisforge --help` for the full option list. Then, inside the generated project folder:
|
||||
double-click `build.bat` on Windows, or run `bash build.sh` on Linux/WSL.
|
||||
|
||||
## Building from source
|
||||
|
||||
```
|
||||
git clone --recurse-submodules https://github.com/xeghosted/OrbisForge.git
|
||||
cd OrbisForge
|
||||
dotnet build OrbisForge.slnx
|
||||
```
|
||||
|
||||
`--recurse-submodules` is required — the GUI depends on
|
||||
[UIFramework](https://github.com/xeghosted/UIFramework), pulled in as a git submodule at
|
||||
`external/UIFramework`. If you already cloned without it: `git submodule update --init`.
|
||||
|
||||
To publish self-contained CLI binaries:
|
||||
|
||||
```
|
||||
dotnet publish src/OrbisForge.Cli -c Release -r win-x64 --self-contained -p:PublishSingleFile=true -o publish/win-x64
|
||||
dotnet publish src/OrbisForge.Cli -c Release -r linux-x64 --self-contained -p:PublishSingleFile=true -o publish/linux-x64
|
||||
```
|
||||
|
||||
## Project layout
|
||||
|
||||
- `src/OrbisForge.Core` — scaffolding logic (multi-targeted `net8.0`/`netstandard2.0`)
|
||||
- `src/OrbisForge.Cli` — cross-platform console scaffolder
|
||||
- `src/OrbisForge.Gui` — Windows wizard (WinForms + UIFramework)
|
||||
- `external/UIFramework` — git submodule
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
+1
Submodule external/UIFramework added at c47309784f.
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<AssemblyName>orbisforge</AssemblyName>
|
||||
<RootNamespace>OrbisForge.Cli</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OrbisForge.Core\OrbisForge.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,131 @@
|
||||
using OrbisForge.Core;
|
||||
|
||||
var help = """
|
||||
OrbisForge - project scaffolder for the OpenOrbis PS4 Toolchain (CMake-based)
|
||||
|
||||
Usage:
|
||||
orbisforge new --name <name> --type exe|prx|sprx [options]
|
||||
|
||||
Options:
|
||||
--name <name> Project / target name (required)
|
||||
--type <type> exe, prx, or sprx (required)
|
||||
--output <dir> Output directory (default: current directory)
|
||||
--pkg EXE only: also scaffold a full .pkg installer step
|
||||
--ptype <ptype> Override create-fself -ptype (advanced)
|
||||
--toolchain-path <p> OO_PS4_TOOLCHAIN override, for locating packaging assets
|
||||
when running on Windows (where that env var normally
|
||||
only exists inside WSL). Point this at the toolchain's
|
||||
\\wsl.localhost\... path if using --pkg from Windows.
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
orbisforge new --name hello_cmake --type exe
|
||||
orbisforge new --name my_plugin --type sprx --output E:\Projects\PS4
|
||||
orbisforge new --name my_game --type exe --pkg
|
||||
""";
|
||||
|
||||
if (args.Length == 0 || args[0] is "-h" or "--help" or "help")
|
||||
{
|
||||
Console.WriteLine(help);
|
||||
return args.Length == 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
if (args[0] != "new")
|
||||
{
|
||||
Console.Error.WriteLine($"Unknown command '{args[0]}'. Run 'orbisforge --help'.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
string? name = null;
|
||||
string? typeText = null;
|
||||
string outputDir = Directory.GetCurrentDirectory();
|
||||
bool includePackaging = false;
|
||||
string? ptype = null;
|
||||
string? toolchainPath = null;
|
||||
|
||||
for (int i = 1; i < args.Length; i++)
|
||||
{
|
||||
switch (args[i])
|
||||
{
|
||||
case "--name":
|
||||
name = RequireValue(args, ref i, "--name");
|
||||
break;
|
||||
case "--type":
|
||||
typeText = RequireValue(args, ref i, "--type");
|
||||
break;
|
||||
case "--output":
|
||||
outputDir = RequireValue(args, ref i, "--output");
|
||||
break;
|
||||
case "--pkg":
|
||||
includePackaging = true;
|
||||
break;
|
||||
case "--ptype":
|
||||
ptype = RequireValue(args, ref i, "--ptype");
|
||||
break;
|
||||
case "--toolchain-path":
|
||||
toolchainPath = RequireValue(args, ref i, "--toolchain-path");
|
||||
break;
|
||||
case "-h":
|
||||
case "--help":
|
||||
Console.WriteLine(help);
|
||||
return 0;
|
||||
default:
|
||||
Console.Error.WriteLine($"Unknown option '{args[i]}'.");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
Console.Error.WriteLine("--name is required.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (typeText is null || !OrbisTargetTypeExtensions.TryParse(typeText, out var type))
|
||||
{
|
||||
Console.Error.WriteLine("--type is required and must be one of: exe, prx, sprx.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (includePackaging && type != OrbisTargetType.Exe)
|
||||
{
|
||||
Console.Error.WriteLine("--pkg is only valid with --type exe.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var options = new ProjectOptions
|
||||
{
|
||||
Name = name,
|
||||
Type = type,
|
||||
OutputDirectory = outputDir,
|
||||
IncludePackaging = includePackaging,
|
||||
PType = ptype,
|
||||
ToolchainPathOverride = toolchainPath,
|
||||
};
|
||||
|
||||
var result = ProjectGenerator.Generate(options);
|
||||
|
||||
Console.WriteLine($"Generated {options.Type.ToCMakeString()} project at: {result.RootDirectory}");
|
||||
foreach (var file in result.FilesWritten)
|
||||
{
|
||||
Console.WriteLine($" {Path.GetRelativePath(result.RootDirectory, file)}");
|
||||
}
|
||||
|
||||
foreach (var warning in result.Warnings)
|
||||
{
|
||||
Console.WriteLine($"warning: {warning}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(OperatingSystem.IsWindows()
|
||||
? "Next: double-click build.bat inside the project folder."
|
||||
: "Next: run 'bash build.sh' inside the project folder.");
|
||||
|
||||
return 0;
|
||||
|
||||
static string RequireValue(string[] args, ref int i, string option)
|
||||
{
|
||||
if (i + 1 >= args.Length)
|
||||
throw new ArgumentException($"{option} requires a value.");
|
||||
return args[++i];
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;netstandard2.0</TargetFrameworks>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace OrbisForge.Core;
|
||||
|
||||
/// <summary>
|
||||
/// The PS4 artifact kind a generated project produces.
|
||||
/// Maps to create-fself's --eboot vs --lib packaging mode and a default -ptype.
|
||||
/// </summary>
|
||||
public enum OrbisTargetType
|
||||
{
|
||||
/// <summary>Executable homebrew app -> eboot.bin (ptype "fake").</summary>
|
||||
Exe,
|
||||
|
||||
/// <summary>User-mode relocatable library -> <name>.prx (ptype "fake").</summary>
|
||||
Prx,
|
||||
|
||||
/// <summary>Signed system library/module -> <name>.sprx (ptype "system_dynlib").</summary>
|
||||
Sprx,
|
||||
}
|
||||
|
||||
public static class OrbisTargetTypeExtensions
|
||||
{
|
||||
public static string ToCMakeString(this OrbisTargetType type) => type switch
|
||||
{
|
||||
OrbisTargetType.Exe => "EXE",
|
||||
OrbisTargetType.Prx => "PRX",
|
||||
OrbisTargetType.Sprx => "SPRX",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
|
||||
};
|
||||
|
||||
public static bool IsLibrary(this OrbisTargetType type) =>
|
||||
type is OrbisTargetType.Prx or OrbisTargetType.Sprx;
|
||||
|
||||
public static bool TryParse(string text, out OrbisTargetType type)
|
||||
{
|
||||
switch (text.Trim().ToUpperInvariant())
|
||||
{
|
||||
case "EXE": case "BIN": type = OrbisTargetType.Exe; return true;
|
||||
case "PRX": type = OrbisTargetType.Prx; return true;
|
||||
case "SPRX": type = OrbisTargetType.Sprx; return true;
|
||||
default: type = default; return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#if NETSTANDARD2_0
|
||||
// `init` accessors need this marker type, which only ships in-box starting with
|
||||
// net5.0. netstandard2.0 (used so this library can be referenced by both the
|
||||
// modern .NET CLI and the net48 WinForms GUI) needs it supplied manually.
|
||||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
internal static class IsExternalInit
|
||||
{
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,105 @@
|
||||
using OrbisForge.Core.Templates;
|
||||
|
||||
namespace OrbisForge.Core;
|
||||
|
||||
public sealed class GeneratedProject
|
||||
{
|
||||
public string RootDirectory { get; set; } = "";
|
||||
public IReadOnlyList<string> FilesWritten { get; set; } = new List<string>();
|
||||
public IReadOnlyList<string> Warnings { get; set; } = new List<string>();
|
||||
}
|
||||
|
||||
public static class ProjectGenerator
|
||||
{
|
||||
public static GeneratedProject Generate(ProjectOptions options)
|
||||
{
|
||||
var name = options.SanitizedName;
|
||||
var root = Path.Combine(options.OutputDirectory, name);
|
||||
var files = new List<string>();
|
||||
var warnings = new List<string>();
|
||||
|
||||
Directory.CreateDirectory(root);
|
||||
Directory.CreateDirectory(Path.Combine(root, "cmake"));
|
||||
Directory.CreateDirectory(Path.Combine(root, "src"));
|
||||
|
||||
WriteFile(Path.Combine(root, "cmake", "oo-ps4-toolchain.cmake"), ToolchainTemplate.ToolchainCMake, files);
|
||||
WriteFile(Path.Combine(root, "cmake", "OrbisTargets.cmake"), OrbisTargetsTemplate.OrbisTargetsCMake, files);
|
||||
WriteFile(Path.Combine(root, "CMakeLists.txt"), CMakeListsTemplate.Render(options), files);
|
||||
WriteFile(Path.Combine(root, "CMakePresets.json"), CMakePresetsTemplate.CMakePresetsJson, files);
|
||||
WriteFile(Path.Combine(root, "README.md"), ReadmeTemplate.Render(options), files);
|
||||
WriteFile(Path.Combine(root, "build.sh"), Unix(BuildScriptsTemplate.BuildSh), files);
|
||||
WriteFile(Path.Combine(root, "build.bat"), Windows(BuildScriptsTemplate.BuildBat), files);
|
||||
|
||||
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);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteFile(Path.Combine(root, "src", "main.cpp"), SourceTemplate.MainCpp(options.Name), files);
|
||||
}
|
||||
|
||||
if (options.IncludePackaging && options.Type == OrbisTargetType.Exe)
|
||||
{
|
||||
CopyPackagingAssets(root, options.ToolchainPathOverride, files, warnings);
|
||||
}
|
||||
|
||||
return new GeneratedProject
|
||||
{
|
||||
RootDirectory = root,
|
||||
FilesWritten = files,
|
||||
Warnings = warnings,
|
||||
};
|
||||
}
|
||||
|
||||
private static void CopyPackagingAssets(string root, string? toolchainOverride, List<string> files, List<string> warnings)
|
||||
{
|
||||
var toolchain = toolchainOverride ?? Environment.GetEnvironmentVariable("OO_PS4_TOOLCHAIN");
|
||||
if (string.IsNullOrEmpty(toolchain))
|
||||
{
|
||||
warnings.Add("OO_PS4_TOOLCHAIN is not set in this environment -- skipped copying sce_sys/sce_module " +
|
||||
"packaging assets from the hello_world sample. Copy them manually before building the " +
|
||||
"*_pkg target (see the toolchain's samples/hello_world directory).");
|
||||
Directory.CreateDirectory(Path.Combine(root, "sce_sys", "about"));
|
||||
Directory.CreateDirectory(Path.Combine(root, "sce_module"));
|
||||
Directory.CreateDirectory(Path.Combine(root, "assets"));
|
||||
return;
|
||||
}
|
||||
|
||||
var sampleRoot = Path.Combine(toolchain, "samples", "hello_world");
|
||||
var pairs = new (string From, string To)[]
|
||||
{
|
||||
(Path.Combine(sampleRoot, "sce_sys", "icon0.png"), Path.Combine(root, "sce_sys", "icon0.png")),
|
||||
(Path.Combine(sampleRoot, "sce_sys", "about", "right.sprx"), Path.Combine(root, "sce_sys", "about", "right.sprx")),
|
||||
(Path.Combine(sampleRoot, "sce_module", "libSceFios2.prx"), Path.Combine(root, "sce_module", "libSceFios2.prx")),
|
||||
(Path.Combine(sampleRoot, "sce_module", "libc.prx"), Path.Combine(root, "sce_module", "libc.prx")),
|
||||
};
|
||||
|
||||
foreach (var (from, to) in pairs)
|
||||
{
|
||||
if (!File.Exists(from))
|
||||
{
|
||||
warnings.Add($"Packaging asset not found, skipped: {from}");
|
||||
continue;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(to)!);
|
||||
File.Copy(from, to, overwrite: true);
|
||||
files.Add(to);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.Combine(root, "assets"));
|
||||
}
|
||||
|
||||
private static void WriteFile(string path, string content, List<string> files)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
File.WriteAllText(path, content);
|
||||
files.Add(path);
|
||||
}
|
||||
|
||||
private static string Unix(string text) => text.Replace("\r\n", "\n");
|
||||
|
||||
private static string Windows(string text) => Unix(text).Replace("\n", "\r\n");
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace OrbisForge.Core;
|
||||
|
||||
/// <summary>Everything needed to scaffold one OpenOrbis CMake project.</summary>
|
||||
public sealed class ProjectOptions
|
||||
{
|
||||
/// <summary>Project / target name. Used as the CMake target name and output file stem.</summary>
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
public OrbisTargetType Type { get; set; }
|
||||
|
||||
/// <summary>Directory the project is written into (created if missing).</summary>
|
||||
public string OutputDirectory { get; set; } = ".";
|
||||
|
||||
/// <summary>EXE projects only: also scaffold the full retail-style .pkg packaging step
|
||||
/// (param.sfo + create-gp4 + PkgTool.Core pkg_build), not just eboot.bin.</summary>
|
||||
public bool IncludePackaging { get; set; }
|
||||
|
||||
/// <summary>Override for create-fself's -ptype. Null uses the type's default
|
||||
/// ("fake" for EXE/PRX, "system_dynlib" for SPRX).</summary>
|
||||
public string? PType { get; set; }
|
||||
|
||||
/// <summary>Explicit path to the OpenOrbis toolchain root, used only for locating
|
||||
/// packaging assets (icon0.png, sce_module stubs) to copy from samples/hello_world
|
||||
/// when IncludePackaging is set. Falls back to the OO_PS4_TOOLCHAIN environment
|
||||
/// variable when null -- callers running as a Windows process (where that variable
|
||||
/// only exists inside WSL, not on the Windows side) should resolve and pass the
|
||||
/// \\wsl.localhost\... UNC equivalent here instead.</summary>
|
||||
public string? ToolchainPathOverride { get; set; }
|
||||
|
||||
public string SanitizedName => Sanitize(Name);
|
||||
|
||||
private static string Sanitize(string name)
|
||||
{
|
||||
var chars = name.Trim().Select(c => char.IsLetterOrDigit(c) || c is '_' or '-' ? c : '_').ToArray();
|
||||
var result = new string(chars);
|
||||
return result.Length == 0 ? "orbis_project" : result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
namespace OrbisForge.Core.Templates;
|
||||
|
||||
internal static class BuildScriptsTemplate
|
||||
{
|
||||
// Runs natively on Linux/WSL. If invoked from an interactive shell, .bashrc has
|
||||
// already exported OO_PS4_TOOLCHAIN/PATH normally -- no special handling needed.
|
||||
public const string BuildSh = """
|
||||
#!/bin/bash
|
||||
set -e
|
||||
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
if [ -z "$OO_PS4_TOOLCHAIN" ]; then
|
||||
echo "OO_PS4_TOOLCHAIN environment variable is not set. See the OpenOrbis PS4 Toolchain README." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$1" = "clean" ]; then
|
||||
rm -rf build
|
||||
echo "Cleaned."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=cmake/oo-ps4-toolchain.cmake
|
||||
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.
|
||||
public const string BuildBat = """
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
set "SCRIPTDIR=%~dp0"
|
||||
if "%SCRIPTDIR:~-1%"=="\" set "SCRIPTDIR=%SCRIPTDIR:~0,-1%"
|
||||
|
||||
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.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
for /f "usebackq delims=" %%i in (`wsl wslpath "%SCRIPTDIR%"`) do set "WSLDIR=%%i"
|
||||
|
||||
if "%~1"=="clean" (
|
||||
wsl -- bash -c "rm -rf '%WSLDIR%/build'"
|
||||
echo Cleaned.
|
||||
goto :eof
|
||||
)
|
||||
|
||||
wsl -- bash -c "export OO_PS4_TOOLCHAIN=%OO_TOOLCHAIN_WSL%; export PATH=%OO_TOOLCHAIN_WSL%/bin/linux:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1; cd '%WSLDIR%' && cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=cmake/oo-ps4-toolchain.cmake && cmake --build build"
|
||||
""";
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Text;
|
||||
|
||||
namespace OrbisForge.Core.Templates;
|
||||
|
||||
internal static class CMakeListsTemplate
|
||||
{
|
||||
public static string Render(ProjectOptions options)
|
||||
{
|
||||
var name = options.SanitizedName;
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine("cmake_minimum_required(VERSION 3.20)");
|
||||
sb.AppendLine($"project({name} CXX)");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("if(NOT DEFINED ENV{OO_PS4_TOOLCHAIN})");
|
||||
sb.AppendLine(" message(FATAL_ERROR \"OO_PS4_TOOLCHAIN environment variable is not set\")");
|
||||
sb.AppendLine("endif()");
|
||||
sb.AppendLine();
|
||||
|
||||
var sources = options.Type.IsLibrary()
|
||||
? $"src/{name}.cpp"
|
||||
: "src/main.cpp";
|
||||
|
||||
var ptypeArg = options.PType is null ? "" : $"\n PTYPE {options.PType}";
|
||||
|
||||
sb.AppendLine($"""
|
||||
add_orbis_target({name}
|
||||
TYPE {options.Type.ToCMakeString()}
|
||||
SOURCES {sources}{ptypeArg}
|
||||
)
|
||||
""");
|
||||
|
||||
if (options.IncludePackaging && options.Type == OrbisTargetType.Exe)
|
||||
{
|
||||
var upper = name.ToUpperInvariant();
|
||||
var titleId = ("OF" + upper).PadRight(9, '0').Substring(0, 9);
|
||||
// Sony's content-ID shape: IV0000-<9-char title id>_00-<16-char suffix>.
|
||||
var contentId = $"IV0000-{titleId}_00-{upper.PadRight(16, '0').Substring(0, 16)}";
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"""
|
||||
add_orbis_package({name}
|
||||
TITLE_ID {titleId}
|
||||
CONTENT_ID {contentId}
|
||||
TITLE "{options.Name}"
|
||||
VERSION "1.00"
|
||||
)
|
||||
""");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace OrbisForge.Core.Templates;
|
||||
|
||||
internal static class CMakePresetsTemplate
|
||||
{
|
||||
public const string CMakePresetsJson = """
|
||||
{
|
||||
"version": 6,
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "default",
|
||||
"displayName": "OpenOrbis (Linux/WSL native)",
|
||||
"binaryDir": "${sourceDir}/build",
|
||||
"cacheVariables": {
|
||||
"CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/oo-ps4-toolchain.cmake"
|
||||
}
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{
|
||||
"name": "default",
|
||||
"configurePreset": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
namespace OrbisForge.Core.Templates;
|
||||
|
||||
internal static class OrbisTargetsTemplate
|
||||
{
|
||||
public const string OrbisTargetsCMake = """
|
||||
# add_orbis_target() / add_orbis_package() - generated by OrbisForge.
|
||||
#
|
||||
# add_orbis_target(<name>
|
||||
# TYPE EXE|PRX|SPRX
|
||||
# SOURCES <files...>
|
||||
# [LIBS <names...>] # default: c kernel c++
|
||||
# [PTYPE <ptype>] # default: "fake" (EXE/PRX) or "system_dynlib" (SPRX)
|
||||
# [PAID <hex>] # default: 0x3800000000000011
|
||||
# [LIBNAME <name>] # PRX/SPRX 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
|
||||
# readoelf then runs over the packaged .oelf as a build-time sanity check.
|
||||
|
||||
function(add_orbis_target name)
|
||||
set(options "")
|
||||
set(oneValueArgs TYPE PTYPE PAID LIBNAME)
|
||||
set(multiValueArgs SOURCES LIBS)
|
||||
cmake_parse_arguments(ORBIS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
|
||||
|
||||
if(NOT ORBIS_SOURCES)
|
||||
message(FATAL_ERROR "add_orbis_target(${name}): SOURCES is required")
|
||||
endif()
|
||||
|
||||
if(NOT ORBIS_TYPE)
|
||||
set(ORBIS_TYPE "EXE")
|
||||
endif()
|
||||
string(TOUPPER "${ORBIS_TYPE}" ORBIS_TYPE)
|
||||
|
||||
if(NOT ORBIS_LIBS)
|
||||
set(ORBIS_LIBS c kernel c++)
|
||||
endif()
|
||||
|
||||
add_executable(${name} ${ORBIS_SOURCES})
|
||||
|
||||
if(ORBIS_TYPE STREQUAL "EXE")
|
||||
set(_crt_object "${OO_PS4_TOOLCHAIN}/lib/crt1.o")
|
||||
set(_default_ptype "fake")
|
||||
set(_out_name "eboot.bin")
|
||||
set(_pkg_flag "--eboot")
|
||||
elseif(ORBIS_TYPE STREQUAL "PRX")
|
||||
set(_crt_object "${OO_PS4_TOOLCHAIN}/lib/crtlib.o")
|
||||
set(_default_ptype "fake")
|
||||
set(_out_name "${name}.prx")
|
||||
set(_pkg_flag "--lib")
|
||||
elseif(ORBIS_TYPE STREQUAL "SPRX")
|
||||
set(_crt_object "${OO_PS4_TOOLCHAIN}/lib/crtlib.o")
|
||||
set(_default_ptype "system_dynlib")
|
||||
set(_out_name "${name}.sprx")
|
||||
set(_pkg_flag "--lib")
|
||||
else()
|
||||
message(FATAL_ERROR "add_orbis_target(${name}): TYPE must be EXE, PRX, or SPRX (got '${ORBIS_TYPE}')")
|
||||
endif()
|
||||
|
||||
# Order matters here and mirrors the toolchain's own sample Makefiles:
|
||||
# object files, then -l libs, then the CRT object last, so crt1.o's own
|
||||
# references into libkernel resolve against a linker that has already
|
||||
# seen -lkernel.
|
||||
target_link_libraries(${name} PRIVATE ${ORBIS_LIBS} "${_crt_object}")
|
||||
|
||||
if(ORBIS_PTYPE)
|
||||
set(_ptype "${ORBIS_PTYPE}")
|
||||
else()
|
||||
set(_ptype "${_default_ptype}")
|
||||
endif()
|
||||
|
||||
if(NOT ORBIS_PAID)
|
||||
set(ORBIS_PAID "0x3800000000000011")
|
||||
endif()
|
||||
|
||||
set(_libname_args "")
|
||||
if(ORBIS_LIBNAME)
|
||||
set(_libname_args -libname "${ORBIS_LIBNAME}")
|
||||
endif()
|
||||
|
||||
set(_out_path "${CMAKE_CURRENT_BINARY_DIR}/${_out_name}")
|
||||
|
||||
add_custom_command(TARGET ${name} POST_BUILD
|
||||
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/create-fself"
|
||||
-in=$<TARGET_FILE:${name}>
|
||||
-out=$<TARGET_FILE:${name}>.oelf
|
||||
${_pkg_flag} "${_out_path}"
|
||||
-ptype ${_ptype}
|
||||
--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})"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
set_target_properties(${name} PROPERTIES
|
||||
ORBIS_OUTPUT_PATH "${_out_path}"
|
||||
ORBIS_OUTPUT_NAME "${_out_name}"
|
||||
ORBIS_TYPE "${ORBIS_TYPE}"
|
||||
)
|
||||
endfunction()
|
||||
|
||||
# add_orbis_package(<target>
|
||||
# TITLE_ID <id> CONTENT_ID <id> TITLE <string> [VERSION <string>]
|
||||
# [ASSETS_DIR <dir>] [SCE_MODULE_DIR <dir>]
|
||||
# )
|
||||
#
|
||||
# Builds a full retail-style .pkg installer from an EXE target created by
|
||||
# add_orbis_target(): param.sfo -> pkg.gp4 -> <content-id>.pkg, exactly
|
||||
# mirroring the toolchain's own sample Makefiles. Adds a "<target>_pkg"
|
||||
# target you build explicitly (it is not part of the default `all`).
|
||||
function(add_orbis_package target)
|
||||
set(oneValueArgs TITLE_ID CONTENT_ID TITLE VERSION ASSETS_DIR SCE_MODULE_DIR)
|
||||
cmake_parse_arguments(PKG "" "${oneValueArgs}" "" ${ARGN})
|
||||
|
||||
if(NOT PKG_TITLE_ID OR NOT PKG_CONTENT_ID OR NOT PKG_TITLE)
|
||||
message(FATAL_ERROR "add_orbis_package(${target}): TITLE_ID, CONTENT_ID, and TITLE are required")
|
||||
endif()
|
||||
if(NOT PKG_VERSION)
|
||||
set(PKG_VERSION "1.00")
|
||||
endif()
|
||||
if(NOT PKG_ASSETS_DIR)
|
||||
set(PKG_ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/assets")
|
||||
endif()
|
||||
if(NOT PKG_SCE_MODULE_DIR)
|
||||
set(PKG_SCE_MODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/sce_module")
|
||||
endif()
|
||||
|
||||
get_target_property(_eboot ${target} ORBIS_OUTPUT_PATH)
|
||||
if(NOT _eboot)
|
||||
message(FATAL_ERROR "add_orbis_package(${target}): target has no ORBIS_OUTPUT_PATH; call add_orbis_target(${target} TYPE EXE ...) first")
|
||||
endif()
|
||||
|
||||
set(_sce_sys "${CMAKE_CURRENT_SOURCE_DIR}/sce_sys")
|
||||
|
||||
# create-gp4's --files list has no separate source-vs-target-path syntax:
|
||||
# each entry is a path relative to the working directory it runs from,
|
||||
# and that SAME relative path becomes the file's location inside the
|
||||
# package. eboot.bin and param.sfo are built elsewhere (under
|
||||
# CMAKE_CURRENT_BINARY_DIR), so they need to be staged into a directory
|
||||
# tree that already looks like the final package layout before create-gp4
|
||||
# runs from it -- otherwise the package ends up with eboot.bin nested
|
||||
# under a stray "build/" path instead of at its root, where the PS4
|
||||
# loader requires it.
|
||||
set(_stage "${CMAKE_CURRENT_BINARY_DIR}/pkg_root")
|
||||
set(_sfo "${_stage}/sce_sys/param.sfo")
|
||||
set(_gp4 "${_stage}/pkg.gp4")
|
||||
# PkgTool.Core's pkg_build takes the gp4 path plus a "project directory"
|
||||
# it resolves the gp4's relative file entries against AND writes the
|
||||
# final .pkg into -- that has to be the same staging directory too.
|
||||
set(_pkg_out "${_stage}/${PKG_CONTENT_ID}.pkg")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${_sfo}"
|
||||
COMMAND "${CMAKE_COMMAND}" -E make_directory "${_stage}/sce_sys/about" "${_stage}/sce_module" "${_stage}/assets"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy "${_eboot}" "${_stage}/eboot.bin"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${_sce_sys}/icon0.png" "${_stage}/sce_sys/icon0.png"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${_sce_sys}/about/right.sprx" "${_stage}/sce_sys/about/right.sprx"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_directory "${PKG_SCE_MODULE_DIR}" "${_stage}/sce_module"
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_directory "${PKG_ASSETS_DIR}" "${_stage}/assets"
|
||||
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_new "${_sfo}"
|
||||
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" APP_TYPE --type Integer --maxsize 4 --value 1
|
||||
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" APP_VER --type Utf8 --maxsize 8 --value "${PKG_VERSION}"
|
||||
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" ATTRIBUTE --type Integer --maxsize 4 --value 0
|
||||
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" CATEGORY --type Utf8 --maxsize 4 --value gd
|
||||
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" CONTENT_ID --type Utf8 --maxsize 48 --value "${PKG_CONTENT_ID}"
|
||||
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" DOWNLOAD_DATA_SIZE --type Integer --maxsize 4 --value 0
|
||||
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" SYSTEM_VER --type Integer --maxsize 4 --value 0
|
||||
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" TITLE --type Utf8 --maxsize 128 --value "${PKG_TITLE}"
|
||||
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" TITLE_ID --type Utf8 --maxsize 12 --value "${PKG_TITLE_ID}"
|
||||
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" VERSION --type Utf8 --maxsize 8 --value "${PKG_VERSION}"
|
||||
DEPENDS "${target}"
|
||||
COMMENT "orbis: staging package contents + generating param.sfo for ${target}"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
file(GLOB_RECURSE _assets RELATIVE "${PKG_ASSETS_DIR}" CONFIGURE_DEPENDS "${PKG_ASSETS_DIR}/*")
|
||||
file(GLOB _modules RELATIVE "${PKG_SCE_MODULE_DIR}" CONFIGURE_DEPENDS "${PKG_SCE_MODULE_DIR}/*")
|
||||
list(TRANSFORM _assets PREPEND "assets/")
|
||||
list(TRANSFORM _modules PREPEND "sce_module/")
|
||||
set(_all_files eboot.bin sce_sys/about/right.sprx sce_sys/param.sfo sce_sys/icon0.png ${_modules} ${_assets})
|
||||
list(JOIN _all_files " " _files_arg)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${_gp4}"
|
||||
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/create-gp4"
|
||||
-out "${_gp4}"
|
||||
--content-id=${PKG_CONTENT_ID}
|
||||
--files "${_files_arg}"
|
||||
WORKING_DIRECTORY "${_stage}"
|
||||
DEPENDS "${_sfo}"
|
||||
COMMENT "orbis: generating pkg.gp4 for ${target}"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${_pkg_out}"
|
||||
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" pkg_build "${_gp4}" "${_stage}"
|
||||
WORKING_DIRECTORY "${_stage}"
|
||||
DEPENDS "${_gp4}"
|
||||
COMMENT "orbis: building ${PKG_CONTENT_ID}.pkg"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_custom_target(${target}_pkg DEPENDS "${_pkg_out}")
|
||||
set_target_properties(${target}_pkg PROPERTIES ORBIS_PKG_PATH "${_pkg_out}")
|
||||
endfunction()
|
||||
""";
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace OrbisForge.Core.Templates;
|
||||
|
||||
internal static class ReadmeTemplate
|
||||
{
|
||||
public static string Render(ProjectOptions options)
|
||||
{
|
||||
var name = options.SanitizedName;
|
||||
var outputs = options.Type switch
|
||||
{
|
||||
OrbisTargetType.Exe => "build/eboot.bin",
|
||||
OrbisTargetType.Prx => $"build/{name}.prx",
|
||||
OrbisTargetType.Sprx => $"build/{name}.sprx",
|
||||
_ => "build/",
|
||||
};
|
||||
|
||||
var pkgNote = options.IncludePackaging && options.Type == OrbisTargetType.Exe
|
||||
? "\nTo build the full retail-style installer: `cmake --build build --target {0}_pkg` (Windows: `wsl -- bash -c \"cd '<wsl-path>' && cmake --build build --target {0}_pkg\"`). Produces `build/pkg/*.pkg`.\n"
|
||||
: "";
|
||||
pkgNote = string.Format(pkgNote, name);
|
||||
|
||||
return $"""
|
||||
# {options.Name}
|
||||
|
||||
Generated by OrbisForge for the [OpenOrbis PS4 Toolchain](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain).
|
||||
|
||||
Type: **{options.Type.ToCMakeString()}** -> `{outputs}`
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `OO_PS4_TOOLCHAIN` environment variable set (in WSL, if building from Windows)
|
||||
- clang + lld + cmake available on the build machine (WSL/Linux)
|
||||
|
||||
## Building
|
||||
|
||||
**Windows:** double-click `build.bat`, or `build.bat clean` to wipe the build directory.
|
||||
|
||||
**Linux / WSL (native):** `bash build.sh`, or `bash build.sh clean` (use `bash` rather than `./build.sh` since files written from Windows don't carry the executable bit).
|
||||
{pkgNote}
|
||||
## Project layout
|
||||
|
||||
- `src/` - source files
|
||||
- `cmake/oo-ps4-toolchain.cmake` - CMake toolchain file for the OpenOrbis clang/lld setup
|
||||
- `cmake/OrbisTargets.cmake` - `add_orbis_target()` / `add_orbis_package()` helpers
|
||||
- `CMakeLists.txt` - project definition
|
||||
""";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace OrbisForge.Core.Templates;
|
||||
|
||||
internal static class SourceTemplate
|
||||
{
|
||||
public static string MainCpp(string projectName) => $$"""
|
||||
#include <stdio.h>
|
||||
#include <orbis/libkernel.h>
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
|
||||
printf("Hello from {{projectName}}!\n");
|
||||
sceKernelUsleep(2 * 1000000);
|
||||
|
||||
return 0;
|
||||
}
|
||||
""";
|
||||
|
||||
public static string LibCpp(string projectName) => $$"""
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sstream>
|
||||
|
||||
#include "{{projectName}}.h"
|
||||
|
||||
int {{projectName}}_hello(char *buf, size_t buf_size, int num)
|
||||
{
|
||||
std::ostringstream os;
|
||||
os << "Hi from {{projectName}}! You passed: " << num;
|
||||
std::string s = os.str();
|
||||
strncpy(buf, s.c_str(), buf_size);
|
||||
|
||||
return 0;
|
||||
}
|
||||
""";
|
||||
|
||||
public static string LibHeader(string projectName) => $$"""
|
||||
#pragma once
|
||||
#include <stddef.h>
|
||||
|
||||
int {{projectName}}_hello(char *buf, size_t buf_size, int num);
|
||||
""";
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
namespace OrbisForge.Core.Templates;
|
||||
|
||||
internal static class ToolchainTemplate
|
||||
{
|
||||
public const string ToolchainCMake = """
|
||||
# CMake toolchain file for the OpenOrbis PS4 Toolchain.
|
||||
# Generated by OrbisForge. Requires the OO_PS4_TOOLCHAIN environment variable.
|
||||
#
|
||||
# Usage: cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=cmake/oo-ps4-toolchain.cmake
|
||||
|
||||
set(CMAKE_SYSTEM_NAME Generic)
|
||||
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||
|
||||
if(NOT DEFINED ENV{OO_PS4_TOOLCHAIN})
|
||||
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")
|
||||
|
||||
set(CMAKE_C_COMPILER clang)
|
||||
set(CMAKE_CXX_COMPILER clang++)
|
||||
|
||||
set(CMAKE_C_COMPILER_TARGET x86_64-pc-freebsd12-elf)
|
||||
set(CMAKE_CXX_COMPILER_TARGET x86_64-pc-freebsd12-elf)
|
||||
|
||||
# CMake's compiler sanity check tries to link a full executable, which fails
|
||||
# against this target unless we skip straight to a static-library try-compile.
|
||||
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
|
||||
|
||||
set(_oo_common_flags "-fPIC -funwind-tables -isysroot ${OO_PS4_TOOLCHAIN} -isystem ${OO_PS4_TOOLCHAIN}/include")
|
||||
set(CMAKE_C_FLAGS_INIT "${_oo_common_flags}")
|
||||
set(CMAKE_CXX_FLAGS_INIT "${_oo_common_flags} -isystem ${OO_PS4_TOOLCHAIN}/include/c++/v1")
|
||||
|
||||
# Base linker setup shared by every target. The CRT start object and the
|
||||
# -l libs are added per-target by add_orbis_target() below, since
|
||||
# executables and libraries need different CRT objects (crt1.o vs crtlib.o).
|
||||
set(CMAKE_EXE_LINKER_FLAGS_INIT
|
||||
"-fuse-ld=lld -nostdlib -nostartfiles -pie -Wl,--script=${OO_PS4_TOOLCHAIN}/link.x -Wl,--eh-frame-hdr -L${OO_PS4_TOOLCHAIN}/lib"
|
||||
)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/OrbisTargets.cmake")
|
||||
""";
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<AssemblyName>OrbisForge</AssemblyName>
|
||||
<RootNamespace>OrbisForge.Gui</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OrbisForge.Core\OrbisForge.Core.csproj" />
|
||||
<ProjectReference Include="..\..\external\UIFramework\src\UIFramework.Core\UIFramework.Core.csproj" />
|
||||
<ProjectReference Include="..\..\external\UIFramework\src\UIFramework.Controls\UIFramework.Controls.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace OrbisForge.Gui
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
[STAThread]
|
||||
private static void Main()
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.Run(new WizardForm());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using UIFramework.Controls;
|
||||
using UIFramework.Core.Controls;
|
||||
|
||||
using OrbisForge.Core;
|
||||
|
||||
namespace OrbisForge.Gui
|
||||
{
|
||||
internal sealed class WizardForm : SkinnedForm
|
||||
{
|
||||
private readonly SkinTextBox _nameBox = new SkinTextBox();
|
||||
private readonly SkinComboBox _typeBox = new SkinComboBox();
|
||||
private readonly SkinTextBox _outputBox = new SkinTextBox();
|
||||
private readonly CheckBox _pkgCheck = new CheckBox();
|
||||
private readonly SkinLabel _envLabel = new SkinLabel();
|
||||
private readonly SkinButton _generateButton = new SkinButton();
|
||||
private readonly TextBox _statusBox = new TextBox();
|
||||
|
||||
private WslDetectionResult _wsl;
|
||||
|
||||
public WizardForm()
|
||||
{
|
||||
Text = "OrbisForge — OpenOrbis PS4 Project Wizard";
|
||||
ClientSize = new Size(520, 460);
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
AutoScaleMode = AutoScaleMode.Dpi;
|
||||
AutoScaleDimensions = new SizeF(96F, 96F);
|
||||
|
||||
var root = new SkinPanel { Dock = DockStyle.Fill };
|
||||
Controls.Add(root);
|
||||
|
||||
const int left = 16;
|
||||
const int fieldWidth = 320;
|
||||
int y = 16;
|
||||
|
||||
var nameLabel = new SkinLabel { Text = "Project name:", Location = new Point(left, y), AutoSize = true };
|
||||
y += 20;
|
||||
_nameBox.Location = new Point(left, y);
|
||||
_nameBox.Size = new Size(fieldWidth, 24);
|
||||
_nameBox.PlaceholderText = "my_project";
|
||||
y += 36;
|
||||
|
||||
var typeLabel = new SkinLabel { Text = "Type:", Location = new Point(left, y), AutoSize = true };
|
||||
y += 20;
|
||||
_typeBox.Items.Add("EXE (eboot.bin, homebrew app)");
|
||||
_typeBox.Items.Add("PRX (user-mode library)");
|
||||
_typeBox.Items.Add("SPRX (signed system library)");
|
||||
_typeBox.SelectedIndex = 0;
|
||||
_typeBox.Location = new Point(left, y);
|
||||
_typeBox.Size = new Size(fieldWidth, 24);
|
||||
_typeBox.SelectedIndexChanged += (s, e) => UpdatePkgCheckEnabled();
|
||||
y += 36;
|
||||
|
||||
var outputLabel = new SkinLabel { Text = "Output folder:", Location = new Point(left, y), AutoSize = true };
|
||||
y += 20;
|
||||
_outputBox.Location = new Point(left, y);
|
||||
_outputBox.Size = new Size(fieldWidth - 90, 24);
|
||||
_outputBox.Text = Directory.GetCurrentDirectory();
|
||||
var browseButton = new SkinButton { Text = "Browse…", AutoSize = true, Location = new Point(_outputBox.Right + 8, y) };
|
||||
browseButton.Click += (s, e) => BrowseForFolder();
|
||||
y += 36;
|
||||
|
||||
_pkgCheck.Text = "Also generate full .pkg installer (EXE only)";
|
||||
_pkgCheck.Location = new Point(left, y);
|
||||
_pkgCheck.AutoSize = true;
|
||||
y += 28;
|
||||
|
||||
_envLabel.Location = new Point(left, y);
|
||||
_envLabel.AutoSize = true;
|
||||
_envLabel.Text = "Checking WSL / OpenOrbis toolchain…";
|
||||
y += 28;
|
||||
|
||||
_generateButton.Text = "Generate Project";
|
||||
_generateButton.AutoSize = true;
|
||||
_generateButton.Location = new Point(left, y);
|
||||
_generateButton.Click += (s, e) => Generate();
|
||||
y += 44;
|
||||
|
||||
_statusBox.Multiline = true;
|
||||
_statusBox.ReadOnly = true;
|
||||
_statusBox.ScrollBars = ScrollBars.Vertical;
|
||||
_statusBox.Location = new Point(left, y);
|
||||
_statusBox.Size = new Size(480, 160);
|
||||
_statusBox.BorderStyle = BorderStyle.FixedSingle;
|
||||
|
||||
root.Controls.Add(nameLabel);
|
||||
root.Controls.Add(_nameBox);
|
||||
root.Controls.Add(typeLabel);
|
||||
root.Controls.Add(_typeBox);
|
||||
root.Controls.Add(outputLabel);
|
||||
root.Controls.Add(_outputBox);
|
||||
root.Controls.Add(browseButton);
|
||||
root.Controls.Add(_pkgCheck);
|
||||
root.Controls.Add(_envLabel);
|
||||
root.Controls.Add(_generateButton);
|
||||
root.Controls.Add(_statusBox);
|
||||
|
||||
UpdatePkgCheckEnabled();
|
||||
Shown += (s, e) => DetectEnvironment();
|
||||
}
|
||||
|
||||
private void UpdatePkgCheckEnabled()
|
||||
{
|
||||
bool isExe = SelectedType() == OrbisTargetType.Exe;
|
||||
_pkgCheck.Enabled = isExe;
|
||||
if (!isExe) _pkgCheck.Checked = false;
|
||||
}
|
||||
|
||||
private OrbisTargetType SelectedType()
|
||||
{
|
||||
switch (_typeBox.SelectedIndex)
|
||||
{
|
||||
case 1: return OrbisTargetType.Prx;
|
||||
case 2: return OrbisTargetType.Sprx;
|
||||
default: return OrbisTargetType.Exe;
|
||||
}
|
||||
}
|
||||
|
||||
private void DetectEnvironment()
|
||||
{
|
||||
_envLabel.Text = "Checking WSL / OpenOrbis toolchain…";
|
||||
Cursor = Cursors.WaitCursor;
|
||||
try
|
||||
{
|
||||
_wsl = WslEnvironment.Detect();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Cursor = Cursors.Default;
|
||||
}
|
||||
|
||||
if (!_wsl.WslAvailable)
|
||||
{
|
||||
_envLabel.Text = "WSL not detected — install it before building generated projects.";
|
||||
}
|
||||
else if (_wsl.ToolchainFound)
|
||||
{
|
||||
_envLabel.Text = "Found: " + _wsl.DistroName + " — " + _wsl.ToolchainWslPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
_envLabel.Text = "WSL found (" + _wsl.DistroName + "), but OO_PS4_TOOLCHAIN is not set there.";
|
||||
}
|
||||
}
|
||||
|
||||
private void BrowseForFolder()
|
||||
{
|
||||
using (var dialog = new FolderBrowserDialog())
|
||||
{
|
||||
dialog.SelectedPath = _outputBox.Text;
|
||||
if (dialog.ShowDialog(this) == DialogResult.OK)
|
||||
{
|
||||
_outputBox.Text = dialog.SelectedPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Generate()
|
||||
{
|
||||
string name = _nameBox.Text.Trim();
|
||||
if (name.Length == 0)
|
||||
{
|
||||
MessageBox.Show(this, "Enter a project name first.", "OrbisForge", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var type = SelectedType();
|
||||
bool includePkg = _pkgCheck.Checked && type == OrbisTargetType.Exe;
|
||||
|
||||
string toolchainOverride = null;
|
||||
if (includePkg && _wsl != null && _wsl.ToolchainFound)
|
||||
{
|
||||
toolchainOverride = _wsl.ToolchainUncPath;
|
||||
}
|
||||
|
||||
var options = new ProjectOptions
|
||||
{
|
||||
Name = name,
|
||||
Type = type,
|
||||
OutputDirectory = _outputBox.Text,
|
||||
IncludePackaging = includePkg,
|
||||
ToolchainPathOverride = toolchainOverride,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var result = ProjectGenerator.Generate(options);
|
||||
|
||||
_statusBox.Text = "Generated " + type + " project at:\r\n" + result.RootDirectory + "\r\n\r\nFiles:\r\n";
|
||||
foreach (var file in result.FilesWritten)
|
||||
{
|
||||
_statusBox.AppendText(" " + MakeRelative(result.RootDirectory, file) + "\r\n");
|
||||
}
|
||||
foreach (var warning in result.Warnings)
|
||||
{
|
||||
_statusBox.AppendText("\r\nwarning: " + warning + "\r\n");
|
||||
}
|
||||
_statusBox.AppendText("\r\nNext: double-click build.bat inside the project folder.\r\n");
|
||||
|
||||
var openResult = MessageBox.Show(
|
||||
this,
|
||||
"Project generated at:\n" + result.RootDirectory + "\n\nOpen the folder now?",
|
||||
"OrbisForge",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Information);
|
||||
|
||||
if (openResult == DialogResult.Yes)
|
||||
{
|
||||
Process.Start("explorer.exe", "\"" + result.RootDirectory + "\"");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_statusBox.Text = "Error: " + ex.Message;
|
||||
MessageBox.Show(this, ex.Message, "OrbisForge — generation failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private static string MakeRelative(string root, string fullPath)
|
||||
{
|
||||
var rootUri = new Uri(root.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar);
|
||||
var fileUri = new Uri(fullPath);
|
||||
return Uri.UnescapeDataString(rootUri.MakeRelativeUri(fileUri).ToString()).Replace('/', Path.DirectorySeparatorChar);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace OrbisForge.Gui
|
||||
{
|
||||
/// <summary>
|
||||
/// Detects the OpenOrbis toolchain inside WSL from a Windows process, by shelling
|
||||
/// out to wsl.exe (same approach the generated build.bat scripts use) and then
|
||||
/// resolving the WSL-side path to its \\wsl.localhost\... UNC equivalent so a
|
||||
/// Windows process can read files from it directly.
|
||||
/// </summary>
|
||||
internal sealed class WslDetectionResult
|
||||
{
|
||||
public bool WslAvailable;
|
||||
public string DistroName;
|
||||
public string ToolchainWslPath;
|
||||
public string ToolchainUncPath;
|
||||
|
||||
public bool ToolchainFound
|
||||
{
|
||||
get { return !string.IsNullOrEmpty(ToolchainUncPath) && Directory.Exists(ToolchainUncPath); }
|
||||
}
|
||||
}
|
||||
|
||||
internal static class WslEnvironment
|
||||
{
|
||||
public static WslDetectionResult Detect()
|
||||
{
|
||||
var result = new WslDetectionResult();
|
||||
|
||||
string distroList = RunProcess("wsl.exe", "-l -q", 4000);
|
||||
if (distroList == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
result.WslAvailable = true;
|
||||
|
||||
string distro = FirstNonEmptyLine(distroList);
|
||||
if (distro == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
result.DistroName = distro;
|
||||
|
||||
// Deliberately avoids direct "$VAR" expansion inside the quoted wsl.exe
|
||||
// command line -- across the cmd/wsl/bash quoting layers that reliably
|
||||
// comes back empty even though the variable is genuinely set (confirmed
|
||||
// via "env | grep", which does not need nested quoting and works).
|
||||
string envLine = RunProcess("wsl.exe", "-d " + distro + " -- bash -ic \"env | grep ^OO_PS4_TOOLCHAIN=\"", 6000);
|
||||
if (string.IsNullOrEmpty(envLine))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
int eq = envLine.IndexOf('=');
|
||||
if (eq < 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
string toolchainPath = envLine.Substring(eq + 1).Trim();
|
||||
if (toolchainPath.Length == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
result.ToolchainWslPath = toolchainPath;
|
||||
|
||||
// /home/bbc/OpenOrbis-PS4-Toolchain -> \\wsl.localhost\Ubuntu\home\bbc\OpenOrbis-PS4-Toolchain
|
||||
string windowsRelative = toolchainPath.Replace('/', '\\');
|
||||
if (windowsRelative.StartsWith("\\"))
|
||||
{
|
||||
windowsRelative = windowsRelative.Substring(1);
|
||||
}
|
||||
result.ToolchainUncPath = "\\\\wsl.localhost\\" + distro + "\\" + windowsRelative;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string FirstNonEmptyLine(string text)
|
||||
{
|
||||
var lines = text.Replace("\r", "").Split('\n');
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var trimmed = line.Trim();
|
||||
if (trimmed.Length > 0)
|
||||
{
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string RunProcess(string fileName, string arguments, int timeoutMs)
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo(fileName, arguments)
|
||||
{
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
|
||||
using (var process = Process.Start(psi))
|
||||
{
|
||||
string output = process.StandardOutput.ReadToEnd();
|
||||
if (!process.WaitForExit(timeoutMs))
|
||||
{
|
||||
try { process.Kill(); } catch { }
|
||||
return null;
|
||||
}
|
||||
return process.ExitCode == 0 ? output : null;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user