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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Packages/src/Editor/CliOnlyTools~/Launch/Skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ uloop launch [project-path] [options]
| `project-path` | string | Optional. Use only when the target Unity project is not in the current directory. |
| `-r, --restart` | flag | Kill running Unity and restart |
| `-q, --quit` | flag | Kill an existing Unity process for the project without launching |
| `-i, --ignore-compiler-errors` | flag | Continue opening Unity even when the project has compiler errors |
| `--editor-version <version>` | string | Use this Unity Editor version instead of ProjectVersion.txt |
| `-p, --platform <P>` | string | Build target (e.g., StandaloneOSX, Android, iOS) |
| `--max-depth <N>` | number | Search depth when project-path is omitted (default: 3, -1 for unlimited) |

Expand All @@ -41,6 +43,12 @@ uloop launch -r
# Launch with build target
uloop launch -p Android

# Launch even when the project has compiler errors
uloop launch -i

# Launch with a specific installed Editor version
uloop launch --editor-version 6000.0.0f1

# Quit running Unity without launching
uloop launch --quit
```
Expand Down Expand Up @@ -68,4 +76,6 @@ The final JSON payload includes:

- If Unity is already running, focuses the existing window and verifies tool readiness
- If process scan is blocked (e.g. sandboxed `ps`), plain launch falls back to IPC probing; `--restart` and `--quit` still fail because they need the process id
- `-i, --ignore-compiler-errors` only affects new Unity processes; it has no effect when reusing an already-running Editor
- `--editor-version` only affects new Unity processes; use it with `--restart` to replace an already-running Editor
- The command waits until Unity finishes startup and the CLI can connect before returning
2 changes: 1 addition & 1 deletion cli/dispatcher-contract.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"schemaVersion": 1,
"dispatcherVersion": "3.0.1-beta.3",
"dispatcherVersion": "3.0.1-beta.4",
"dispatcherContractVersion": 1
}
3 changes: 3 additions & 0 deletions cli/internal/cli/completion_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ var nativeCommandOptions = map[string][]string{
launchCommandName: {
"--" + projectPathFlagName,
"--delete-recovery",
"--editor-version",
"--ignore-compiler-errors",
"--max-depth",
"--platform",
"--quit",
"--restart",
"-i",
},
installCommandName: {"--" + installDirFlagName},
updateCommandName: {"--" + updateToVersionFlagName},
Expand Down
6 changes: 4 additions & 2 deletions cli/internal/cli/completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"runtime"
"slices"
"strings"
"testing"
)
Expand Down Expand Up @@ -187,8 +188,9 @@ func TestCompletionListOptionsUsesNativeLaunchOptions(t *testing.T) {
}

output := stdout.String()
for _, option := range []string{"--project-path", "--restart", "--quit", "--delete-recovery", "--platform", "--max-depth"} {
if !strings.Contains(output, option) {
listedOptions := strings.Split(strings.TrimSpace(output), "\n")
for _, option := range []string{"--project-path", "--restart", "--quit", "--delete-recovery", "--platform", "--max-depth", "-i", "--ignore-compiler-errors", "--editor-version"} {
if !slices.Contains(listedOptions, option) {
t.Fatalf("launch option %s was not listed: %s", option, output)
}
}
Expand Down
29 changes: 29 additions & 0 deletions cli/internal/cli/dispatcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,35 @@ func TestRunDispatcherLaunchQuitDoesNotRequireProjectPin(t *testing.T) {
}
}

func TestRunDispatcherLaunchOptionsDoNotRequireProjectPin(t *testing.T) {
// Verifies dispatcher-owned launch flags are parsed before project pin resolution.
projectRoot := createDispatcherUnityProject(t)
t.Chdir(t.TempDir())

previousFinder := findRunningUnityProcessForLaunch
findRunningUnityProcessForLaunch = func(context.Context, string) (*unityProcess, error) {
return nil, nil
}
defer func() {
findRunningUnityProcessForLaunch = previousFinder
}()

var stdout bytes.Buffer
var stderr bytes.Buffer
code := RunDispatcher(
context.Background(),
[]string{"launch", "-i", "--editor-version", "6000.0.0f1", projectRoot, "--quit"},
&stdout,
&stderr)

if code != 0 {
t.Fatalf("dispatcher launch failed: code=%d stderr=%s", code, stderr.String())
}
if !bytes.Contains(stdout.Bytes(), []byte(`"Quit": true`)) {
t.Fatalf("dispatcher launch output mismatch: %s", stdout.String())
}
}

func TestRunDispatcherVersionUsesDispatcherVersion(t *testing.T) {
// Verifies the global launcher reports its own dispatcher release version.
t.Chdir(t.TempDir())
Expand Down
2 changes: 2 additions & 0 deletions cli/internal/cli/help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@ func TestRunProjectLocalLaunchHelpShowsGlobalOptions(t *testing.T) {
for _, expected := range []string{
"Usage:",
"uloop launch [options] [project-path]",
"-i, --ignore-compiler-errors",
"--editor-version <version>",
"Global options:",
"--project-path <path>",
} {
Expand Down
84 changes: 56 additions & 28 deletions cli/internal/cli/launch.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,14 @@ var (
var editorVersionPattern = regexp.MustCompile(`(?m)^m_EditorVersion:\s*(.+)$`)

type launchOptions struct {
projectPath string
restart bool
quit bool
deleteRecovery bool
platform string
maxDepth int
projectPath string
restart bool
quit bool
deleteRecovery bool
ignoreCompilerErrors bool
editorVersion string
platform string
maxDepth int
}

func tryHandleLaunchRequest(
Expand Down Expand Up @@ -177,6 +179,10 @@ func handleExistingLaunchProcess(
stderr io.Writer,
) (bool, int) {
if !options.restart && !options.quit {
if options.editorVersion != "" {
writeClassifiedError(stderr, launchEditorVersionRequiresRestartError(options.editorVersion), errorContext{projectRoot: projectRoot, command: launchCommandName})
return true, 1
}
return true, waitForExistingLaunchReadiness(ctx, projectRoot, runningProcess.pid, stdout, stderr)
}
if err := killUnityProcessForLaunch(runningProcess.pid); err != nil {
Expand All @@ -193,6 +199,18 @@ func handleExistingLaunchProcess(
return false, 0
}

func launchEditorVersionRequiresRestartError(editorVersion string) error {
return &argumentError{
message: "--editor-version requires --restart when Unity is already running for this project.",
option: "--editor-version",
received: editorVersion,
command: launchCommandName,
nextActions: []string{
fmt.Sprintf("Run `uloop launch --restart --editor-version %s` to relaunch Unity with the requested Editor version.", editorVersion),
},
}
}

func waitForExistingLaunchReadiness(ctx context.Context, projectRoot string, pid int, stdout io.Writer, stderr io.Writer) int {
logLaunchExistingFocus(ctx, projectRoot, pid)
spinner := newLaunchSpinner(stdout, stderr)
Expand Down Expand Up @@ -227,7 +245,13 @@ func startUnityAndWaitForReadiness(
writeLine(stdout, "")
}

unityPath, err := resolveUnityExecutablePathForLaunch(projectRoot)
unityVersion, err := resolveLaunchEditorVersion(projectRoot, options)
if err != nil {
writeClassifiedError(stderr, err, errorContext{projectRoot: projectRoot, command: launchCommandName})
return 1
}

unityPath, err := resolveUnityExecutablePathForLaunch(unityVersion)
if err != nil {
writeClassifiedError(stderr, err, errorContext{projectRoot: projectRoot, command: launchCommandName})
return 1
Expand All @@ -238,15 +262,10 @@ func startUnityAndWaitForReadiness(

writeLine(stdout, "Opening Unity...")
writeFormat(stdout, "Project Path: %s\n", projectRoot)
writeFormat(stdout, "Detected Unity version: %s\n", readUnityVersionForLog(projectRoot))
writeFormat(stdout, "Detected Unity version: %s\n", unityVersion)
writeLine(stdout, "Unity Hub launch options: none")

launchArgs := []string{"-projectPath", projectRoot}
if options.platform != "" {
launchArgs = append(launchArgs, "-buildTarget", options.platform)
}

command := newUnityLaunchCommand(unityPath, launchArgs)
command := newUnityLaunchCommand(unityPath, buildUnityLaunchArgs(projectRoot, options))
if err := command.Start(); err != nil {
writeClassifiedError(stderr, err, errorContext{projectRoot: projectRoot, command: launchCommandName})
return 1
Expand All @@ -273,6 +292,17 @@ func startUnityAndWaitForReadiness(
return writeLaunchedReadyResponse(stdout, stderr, projectRoot, previousPid, currentPid)
}

func buildUnityLaunchArgs(projectRoot string, options launchOptions) []string {
launchArgs := []string{"-projectPath", projectRoot}
if options.platform != "" {
launchArgs = append(launchArgs, "-buildTarget", options.platform)
}
if options.ignoreCompilerErrors {
launchArgs = append(launchArgs, "-ignorecompilererrors")
}
return launchArgs
}

func newUnityLaunchCommand(unityPath string, launchArgs []string) *exec.Cmd {
command := exec.Command(unityPath, launchArgs...)
command.Env = append(os.Environ(), "MSYS_NO_PATHCONV=1")
Expand Down Expand Up @@ -365,16 +395,18 @@ func resolveLaunchProjectRoot(startPath string, options launchOptions) (string,
return project.FindUnityProjectRootWithin(startPath, options.maxDepth)
}

func resolveUnityExecutablePath(projectRoot string) (string, error) {
version, err := readUnityEditorVersion(projectRoot)
if err != nil {
return "", err
}

func resolveUnityExecutablePath(version string) (string, error) {
candidates := unityExecutableCandidates(version)
return resolveExistingUnityExecutablePath(version, candidates)
}

func resolveLaunchEditorVersion(projectRoot string, options launchOptions) (string, error) {
if options.editorVersion != "" {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
return options.editorVersion, nil
}
return readUnityEditorVersion(projectRoot)
}

func resolveExistingUnityExecutablePath(version string, candidates []string) (string, error) {
for _, candidate := range candidates {
if _, err := os.Stat(candidate); err == nil {
Expand Down Expand Up @@ -430,14 +462,6 @@ func readUnityEditorVersion(projectRoot string) (string, error) {
return version, nil
}

func readUnityVersionForLog(projectRoot string) string {
version, err := readUnityEditorVersion(projectRoot)
if err != nil {
return "unknown"
}
return version
}

func killUnityProcess(pid int) error {
process, err := os.FindProcess(pid)
if err != nil {
Expand All @@ -454,6 +478,10 @@ func printLaunchHelp(stdout io.Writer) {
writeLine(stdout, " -r, --restart Kill an existing Unity process for the project before launching")
writeLine(stdout, " -q, --quit Kill an existing Unity process for the project without launching")
writeLine(stdout, " -d, --delete-recovery Delete Assets/_Recovery before launch")
writeLine(stdout, " -i, --ignore-compiler-errors")
writeLine(stdout, " Continue opening Unity even when the project has compiler errors")
writeLine(stdout, " --editor-version <version>")
writeLine(stdout, " Use this Unity Editor version instead of ProjectVersion.txt")
writeLine(stdout, " -p, --platform <name> Pass Unity -buildTarget when launching")
writeLine(stdout, " --max-depth <n> Accepted for compatibility when searching from the current directory")
writeLine(stdout, "")
Expand Down
14 changes: 14 additions & 0 deletions cli/internal/cli/launch_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ func applyLaunchOption(options *launchOptions, args []string, index int) (int, e
case arg == "-d" || arg == "--delete-recovery":
options.deleteRecovery = true
return index, nil
case arg == "-i" || arg == "--ignore-compiler-errors":
options.ignoreCompilerErrors = true
return index, nil
case arg == "--editor-version" || strings.HasPrefix(arg, "--editor-version="):
return applyLaunchEditorVersionOption(options, args, index)
case isUnsupportedLaunchHubOption(arg):
return index, unsupportedLaunchHubOptionError(arg)
case arg == "-p" || arg == "--platform" || strings.HasPrefix(arg, "--platform="):
Expand Down Expand Up @@ -69,6 +74,15 @@ func applyLaunchPlatformOption(options *launchOptions, args []string, index int)
return nextLaunchOptionIndex(index, consumed), nil
}

func applyLaunchEditorVersionOption(options *launchOptions, args []string, index int) (int, error) {
value, consumed, err := readLaunchOptionValue(args[index], args, index)
if err != nil {
return index, err
}
options.editorVersion = value
return nextLaunchOptionIndex(index, consumed), nil
}

func applyLaunchMaxDepthOption(options *launchOptions, args []string, index int) (int, error) {
value, consumed, err := readLaunchOptionValue(args[index], args, index)
if err != nil {
Expand Down
Loading
Loading