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
5 changes: 0 additions & 5 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,6 @@ jobs:
working-directory: cli/release-automation
run: go run ./cmd/check-release-triggers --base "origin/${{ github.base_ref }}" --head HEAD

- name: Check dispatcher contract
if: github.event_name == 'pull_request'
working-directory: cli/release-automation
run: go run ./cmd/check-dispatcher-contract --base "origin/${{ github.base_ref }}" --head HEAD

- name: Install golangci-lint
run: |
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.0
Expand Down
32 changes: 2 additions & 30 deletions Assets/Tests/Editor/CliInstallationDetectorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,12 @@ public void ParseShellCliInstallationOutput_WhenLegacyCliJsonExists_ReturnsVersi
[Test]
public void ParseShellCliInstallationOutput_WhenDispatcherJsonExists_ReturnsDispatcherDetection()
{
// Verifies setup detection recognizes the dispatcher contract exposed by global uloop.
// Verifies setup detection recognizes the dispatcher release exposed by global uloop.
string output = "__ULOOP_PATH_START__\n"
+ "/Users/ExampleUser/.local/bin/uloop\n"
+ "__ULOOP_PATH_END__\n"
+ "__ULOOP_CONTRACT_START__\n"
+ "{\"DispatcherVersion\":\"3.0.0\",\"DispatcherContractVersion\":1}\n"
+ "{\"DispatcherVersion\":\"3.0.0\"}\n"
+ "__ULOOP_CONTRACT_END__\n"
+ "__ULOOP_CONTRACT_STATUS_START__\n"
+ "0\n"
Expand All @@ -219,34 +219,6 @@ public void ParseShellCliInstallationOutput_WhenDispatcherJsonExists_ReturnsDisp
Assert.That(detection.ExecutablePath, Is.EqualTo("/Users/ExampleUser/.local/bin/uloop"));
}

[Test]
public void ParseShellCliInstallationOutput_WhenDispatcherContractVersionDiffers_ReturnsVersionWithoutProtocol()
{
// Verifies dispatcher setup detection rejects mismatched launcher contract generations.
string output = "__ULOOP_PATH_START__\n"
+ "/Users/ExampleUser/.local/bin/uloop\n"
+ "__ULOOP_PATH_END__\n"
+ "__ULOOP_CONTRACT_START__\n"
+ "{\"DispatcherVersion\":\"3.0.0\",\"DispatcherContractVersion\":2}\n"
+ "__ULOOP_CONTRACT_END__\n"
+ "__ULOOP_CONTRACT_STATUS_START__\n"
+ "0\n"
+ "__ULOOP_CONTRACT_STATUS_END__\n"
+ "__ULOOP_VERSION_START__\n"
+ "3.0.0\n"
+ "__ULOOP_VERSION_END__\n"
+ "__ULOOP_VERSION_STATUS_START__\n"
+ "0\n"
+ "__ULOOP_VERSION_STATUS_END__\n";

CliInstallationDetection detection =
CliInstallationDetector.ParseShellCliInstallationOutput(output);

Assert.That(detection.Version, Is.EqualTo("3.0.0"));
Assert.That(detection.IsDispatcher, Is.False);
Assert.That(detection.ExecutablePath, Is.EqualTo("/Users/ExampleUser/.local/bin/uloop"));
}

[Test]
public void IsShellDetectionUsableForPathSetup_WhenLegacyCliVersionIsHigh_ReturnsFalse()
{
Expand Down
2 changes: 0 additions & 2 deletions Packages/src/Editor/Domain/CliConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ public static class CliConstants
// Why: global uloop is a dispatcher; project-local CLI versions are downloaded separately.
public const string MINIMUM_REQUIRED_DISPATCHER_VERSION = "3.0.1-beta.6";
public const string MINIMUM_REQUIRED_DISPATCHER_RELEASE_TAG = DISPATCHER_RELEASE_TAG_PREFIX + MINIMUM_REQUIRED_DISPATCHER_VERSION;
// Why: dispatcher setup compatibility is a launcher contract generation, not the IPC protocol generation.
public const int REQUIRED_DISPATCHER_CONTRACT_VERSION = 1;
public const string VERSION_FLAG = "--version";
public const string SHORT_VERSION_FLAG = "-v";
public const string JSON_FLAG = "--json";
Expand Down
39 changes: 3 additions & 36 deletions Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ public sealed class CliInstallationDetector : ICliInstallationDetector
private const string VERSION_JSON_PROJECT_RUNNER_VERSION_PROPERTY = "ProjectRunnerVersion";
private const string VERSION_JSON_LEGACY_CLI_VERSION_PROPERTY = "CliVersion";
private const string VERSION_JSON_DISPATCHER_VERSION_PROPERTY = "DispatcherVersion";
private const string VERSION_JSON_DISPATCHER_CONTRACT_VERSION_PROPERTY = "DispatcherContractVersion";

private string _cachedCliVersion;
private bool _cachedCliIsDispatcher;
Expand Down Expand Up @@ -392,15 +391,10 @@ private static CliInstallationDetection ParseCliContractOutput(string output, st

private static CliInstallationDetection ParseDispatcherContract(JObject parsed, string executablePath)
{
// Why: dispatcher compatibility is enforced by the pin's minimumDispatcherVersion (semver floor),
// so identifying the dispatcher only needs its release version.
string dispatcherVersion = parsed[VERSION_JSON_DISPATCHER_VERSION_PROPERTY]?.ToString();
JToken dispatcherContractVersionToken = parsed[VERSION_JSON_DISPATCHER_CONTRACT_VERSION_PROPERTY];
int? dispatcherContractVersion = ReadIntegerToken(dispatcherContractVersionToken);
if (string.IsNullOrEmpty(dispatcherVersion) || dispatcherContractVersion == null)
{
return new CliInstallationDetection(null, executablePath);
}

if (dispatcherContractVersion.Value != CliConstants.REQUIRED_DISPATCHER_CONTRACT_VERSION)
if (string.IsNullOrEmpty(dispatcherVersion))
{
return new CliInstallationDetection(null, executablePath);
}
Expand All @@ -411,33 +405,6 @@ private static CliInstallationDetection ParseDispatcherContract(JObject parsed,
true);
}

private static int? ReadIntegerToken(JToken integerToken)
{
if (integerToken == null || integerToken.Type != JTokenType.Integer)
{
return null;
}

JValue integerValue = integerToken as JValue;
object rawInteger = integerValue?.Value;
if (rawInteger is int integer)
{
return integer;
}

if (!(rawInteger is long longInteger))
{
return null;
}

if (longInteger < int.MinValue || longInteger > int.MaxValue)
{
return null;
}

return (int)longInteger;
}

private static string ExecuteAndGetOutput(ProcessStartInfo startInfo, CancellationToken ct)
{
UnityEngine.Debug.Assert(startInfo != null, "startInfo must not be null");
Expand Down
5 changes: 0 additions & 5 deletions cli/common/clicontract/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (

const (
contractFileName = "contract.json"
schemaVersion = 1
)

//go:embed contract.json
Expand All @@ -22,7 +21,6 @@ var (
)

type Contract struct {
SchemaVersion int `json:"schemaVersion"`
// ProtocolVersion is the C# IPC contract generation this binary speaks. It moves only
// when the Unity package and the CLI can no longer interoperate, never per release.
ProtocolVersion int `json:"protocolVersion"`
Expand Down Expand Up @@ -61,9 +59,6 @@ func parseContract(content []byte) (Contract, error) {
if err := json.Unmarshal(content, &contract); err != nil {
return Contract{}, fmt.Errorf("CLI contract is invalid JSON: %w", err)
}
if contract.SchemaVersion != schemaVersion {
return Contract{}, fmt.Errorf("CLI contract schema version mismatch: %d", contract.SchemaVersion)
}
if err := requireString(contract.ProjectRunnerVersion, "projectRunnerVersion"); err != nil {
return Contract{}, err
}
Expand Down
1 change: 0 additions & 1 deletion cli/common/clicontract/contract.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"schemaVersion": 1,
"protocolVersion": 3,
"projectRunnerVersion": "3.0.0-beta.46"
}
1 change: 1 addition & 0 deletions cli/common/clicontract/contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ func TestCliContractDoesNotDeclareDispatcherReleaseFields(t *testing.T) {
fields := clitest.RequireContractFieldMap(t, contractFiles, contractFileName)
clitest.RequireContractFieldMissing(t, fields, "dispatcherVersion")
clitest.RequireContractFieldMissing(t, fields, "dispatcherContractVersion")
clitest.RequireContractFieldMissing(t, fields, "schemaVersion")
}
14 changes: 2 additions & 12 deletions cli/common/clicore/runner_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,10 @@ import (
clicontract "github.com/hatayama/unity-cli-loop/common/clicontract"
)

// Version returns the project runner release version advertised by CLI commands.
func Version() string {
return clicontract.ProjectRunnerVersion()
}

// ProtocolVersion returns the IPC protocol generation advertised by CLI commands.
func ProtocolVersion() int {
return clicontract.ProtocolVersion()
}

func WriteVersionJSON(stdout io.Writer) {
content, err := json.Marshal(map[string]any{
"ProjectRunnerVersion": Version(),
"ProtocolVersion": ProtocolVersion(),
"ProjectRunnerVersion": clicontract.ProjectRunnerVersion(),
"ProtocolVersion": clicontract.ProtocolVersion(),
})
if err != nil {
panic(err)
Expand Down
5 changes: 3 additions & 2 deletions cli/common/clicore/tool_readiness.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"time"

"github.com/hatayama/unity-cli-loop/common/clicontract"
"github.com/hatayama/unity-cli-loop/common/project"
"github.com/hatayama/unity-cli-loop/common/unityipc"
"github.com/hatayama/unity-cli-loop/common/unityprocess"
Expand Down Expand Up @@ -133,11 +134,11 @@ func probeToolReadiness(ctx context.Context, projectRoot string, executeDynamicC
}

if !executeDynamicCodeAvailable {
_, err := unityipc.NewClient(connection, Version()).Send(probeContext, "get-version", map[string]any{})
_, err := unityipc.NewClient(connection, clicontract.ProjectRunnerVersion()).Send(probeContext, "get-version", map[string]any{})
return err
}

response, err := unityipc.NewClient(connection, Version()).Send(probeContext, "execute-dynamic-code", executeDynamicCodeReadinessProbeParams())
response, err := unityipc.NewClient(connection, clicontract.ProjectRunnerVersion()).Send(probeContext, "execute-dynamic-code", executeDynamicCodeReadinessProbeParams())
if err != nil {
return err
}
Expand Down
11 changes: 0 additions & 11 deletions cli/dispatcher/dispatchercontract/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (

const (
dispatcherContractFileName = "dispatcher-contract.json"
schemaVersion = 1
)

//go:embed dispatcher-contract.json
Expand All @@ -20,13 +19,9 @@ var contractFiles embed.FS
var DispatcherCurrent = mustLoadDispatcherContract()

type DispatcherContract struct {
SchemaVersion int `json:"schemaVersion"`
// DispatcherVersion is the launcher release version. It moves only when the dispatcher
// itself is released, not when project-local CLI releases move.
DispatcherVersion string `json:"dispatcherVersion"`
// DispatcherContractVersion is the launcher capability generation this binary provides.
// It moves only when project pins need a newer dispatcher contract.
DispatcherContractVersion int `json:"dispatcherContractVersion"`
}

func mustLoadDispatcherContract() DispatcherContract {
Expand All @@ -39,13 +34,7 @@ func mustLoadDispatcherContract() DispatcherContract {
if err := json.Unmarshal(content, &contract); err != nil {
panic(fmt.Sprintf("Dispatcher contract is invalid JSON: %v", err))
}
if contract.SchemaVersion != schemaVersion {
panic(fmt.Sprintf("Dispatcher contract schema version mismatch: %d", contract.SchemaVersion))
}
requireString(contract.DispatcherVersion, "dispatcherVersion")
if contract.DispatcherContractVersion < 1 {
panic(fmt.Sprintf("Dispatcher contract dispatcherContractVersion must be at least 1, got %d", contract.DispatcherContractVersion))
}
return contract
}

Expand Down
9 changes: 2 additions & 7 deletions cli/dispatcher/dispatchercontract/contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,6 @@ import (
"github.com/hatayama/unity-cli-loop/common/clitest"
)

func TestCliContractProvidesDispatcherContractVersion(t *testing.T) {
// Verifies that the contract declares which dispatcher capability generation the binary provides.
if DispatcherCurrent.DispatcherContractVersion < 1 {
t.Fatalf("dispatcherContractVersion must be at least 1, got %d", DispatcherCurrent.DispatcherContractVersion)
}
}

func TestDispatcherContractProvidesRuntimeVersion(t *testing.T) {
// Verifies that the launcher owns a release version independent from project-local CLI releases.
clitest.RequireValidContractVersion(t, "dispatcherVersion", DispatcherCurrent.DispatcherVersion)
Expand All @@ -24,4 +17,6 @@ func TestDispatcherContractDoesNotDeclareCliReleaseFields(t *testing.T) {
clitest.RequireContractFieldMissing(t, fields, "projectRunnerVersion")
clitest.RequireContractFieldMissing(t, fields, "cliVersion")
clitest.RequireContractFieldMissing(t, fields, "protocolVersion")
clitest.RequireContractFieldMissing(t, fields, "dispatcherContractVersion")
clitest.RequireContractFieldMissing(t, fields, "schemaVersion")
}
4 changes: 1 addition & 3 deletions cli/dispatcher/dispatchercontract/dispatcher-contract.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
{
"schemaVersion": 1,
"dispatcherVersion": "3.0.1-beta.12",
"dispatcherContractVersion": 1
"dispatcherVersion": "3.0.1-beta.12"
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ func TestRunDispatcherVersionJSONReportsDispatcherIdentity(t *testing.T) {
if payload["DispatcherVersion"] != dispatcherVersion {
t.Fatalf("DispatcherVersion mismatch: %v", payload["DispatcherVersion"])
}
if _, ok := payload["DispatcherContractVersion"]; !ok {
t.Fatalf("DispatcherContractVersion missing: %#v", payload)
if _, ok := payload["DispatcherContractVersion"]; ok {
t.Fatalf("DispatcherContractVersion must not be emitted: %#v", payload)
}
}

Expand Down
3 changes: 2 additions & 1 deletion cli/dispatcher/internal/dispatcher/dispatcher_download.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"strings"
"time"

"github.com/hatayama/unity-cli-loop/common/clicontract"
"github.com/hatayama/unity-cli-loop/common/clicore"
"github.com/hatayama/unity-cli-loop/dispatcher/internal/nativepath"
sharedupdate "github.com/hatayama/unity-cli-loop/dispatcher/internal/update"
Expand Down Expand Up @@ -52,7 +53,7 @@ func resolveDispatcherRealCLI(ctx context.Context, pin dispatcherPin, stderr io.
}

func dispatcherSiblingRealCLIPath(pin dispatcherPin) (string, bool) {
if pin.ProjectRunnerVersion != clicore.Version() {
if pin.ProjectRunnerVersion != clicontract.ProjectRunnerVersion() {
return "", false
}
executablePath, err := os.Executable()
Expand Down
17 changes: 9 additions & 8 deletions cli/dispatcher/internal/dispatcher/dispatcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"testing"
"time"

"github.com/hatayama/unity-cli-loop/common/clicontract"
"github.com/hatayama/unity-cli-loop/common/clicore"
"github.com/hatayama/unity-cli-loop/dispatcher/internal/nativepath"
)
Expand All @@ -37,8 +38,8 @@ func TestRunDispatcherUsesProjectPinAndCachedRealCLI(t *testing.T) {
// Verifies dispatcher reads the project pin and executes the cached real CLI.
projectRoot := createDispatcherUnityProject(t)
cacheRoot := t.TempDir()
writeDispatcherProjectPin(t, projectRoot, clicore.Version())
expectedCLIPath := writeCachedDispatcherRealCLI(t, cacheRoot, clicore.Version())
writeDispatcherProjectPin(t, projectRoot, clicontract.ProjectRunnerVersion())
expectedCLIPath := writeCachedDispatcherRealCLI(t, cacheRoot, clicontract.ProjectRunnerVersion())
t.Setenv(nativepath.CacheDirEnvName, cacheRoot)
t.Setenv(dispatcherDisableSelfUpdateEnvName, "1")
t.Chdir(projectRoot)
Expand Down Expand Up @@ -72,8 +73,8 @@ func TestRunDispatcherPreservesExplicitProjectPathForRealCLI(t *testing.T) {
// Verifies dispatcher accepts trailing --project-path and passes the original arguments onward.
projectRoot := createDispatcherUnityProject(t)
cacheRoot := t.TempDir()
writeDispatcherProjectPin(t, projectRoot, clicore.Version())
writeCachedDispatcherRealCLI(t, cacheRoot, clicore.Version())
writeDispatcherProjectPin(t, projectRoot, clicontract.ProjectRunnerVersion())
writeCachedDispatcherRealCLI(t, cacheRoot, clicontract.ProjectRunnerVersion())
t.Setenv(nativepath.CacheDirEnvName, cacheRoot)
t.Setenv(dispatcherDisableSelfUpdateEnvName, "1")
t.Chdir(t.TempDir())
Expand All @@ -99,8 +100,8 @@ func TestRunDispatcherForwardsProjectScopedVersionToPinnedRunner(t *testing.T) {
// Verifies --project-path --version is forwarded so the pinned runner reports its own version.
projectRoot := createDispatcherUnityProject(t)
cacheRoot := t.TempDir()
writeDispatcherProjectPin(t, projectRoot, clicore.Version())
writeCachedDispatcherRealCLI(t, cacheRoot, clicore.Version())
writeDispatcherProjectPin(t, projectRoot, clicontract.ProjectRunnerVersion())
writeCachedDispatcherRealCLI(t, cacheRoot, clicontract.ProjectRunnerVersion())
t.Setenv(nativepath.CacheDirEnvName, cacheRoot)
t.Setenv(dispatcherDisableSelfUpdateEnvName, "1")
t.Chdir(t.TempDir())
Expand Down Expand Up @@ -129,8 +130,8 @@ func TestRunDispatcherForwardsProjectScopedVersionJSONToPinnedRunner(t *testing.
// Verifies --project-path --version --json is forwarded so the pinned runner reports its own version payload.
projectRoot := createDispatcherUnityProject(t)
cacheRoot := t.TempDir()
writeDispatcherProjectPin(t, projectRoot, clicore.Version())
writeCachedDispatcherRealCLI(t, cacheRoot, clicore.Version())
writeDispatcherProjectPin(t, projectRoot, clicontract.ProjectRunnerVersion())
writeCachedDispatcherRealCLI(t, cacheRoot, clicontract.ProjectRunnerVersion())
t.Setenv(nativepath.CacheDirEnvName, cacheRoot)
t.Setenv(dispatcherDisableSelfUpdateEnvName, "1")
t.Chdir(t.TempDir())
Expand Down
8 changes: 2 additions & 6 deletions cli/dispatcher/internal/dispatcher/dispatcher_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,11 @@ import (
"github.com/hatayama/unity-cli-loop/dispatcher/dispatchercontract"
)

var (
dispatcherVersion = dispatchercontract.DispatcherCurrent.DispatcherVersion
dispatcherContractVersion = dispatchercontract.DispatcherCurrent.DispatcherContractVersion
)
var dispatcherVersion = dispatchercontract.DispatcherCurrent.DispatcherVersion

func writeDispatcherVersionJSON(stdout io.Writer) {
content, err := json.Marshal(map[string]any{
"DispatcherVersion": dispatcherVersion,
"DispatcherContractVersion": dispatcherContractVersion,
"DispatcherVersion": dispatcherVersion,
})
if err != nil {
panic(err)
Expand Down
Loading
Loading