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
27 changes: 27 additions & 0 deletions src/utils/godot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,33 @@ export function getGodotVersion(): string {
return version as string
}

/** Detects the Godot version from project.godot. Returns null when we can't tell. */
export function detectGodotVersion(): null | string {
try {
return getGodotVersion()
} catch {
return null
}
}

export type GodotVersionDrift = 'major' | 'minor'

function parseMajorMinor(version: string): null | {major: number; minor: number} {
const match = version.trim().match(/^(\d+)\.(\d+)/)
if (!match) return null
return {major: Number(match[1]), minor: Number(match[2])}
}

/** Compares on major.minor, ignoring the patch. Null when equal or either is unparseable. */
export function getGodotVersionDrift(detected: string, configured: string): GodotVersionDrift | null {
const a = parseMajorMinor(detected)
const b = parseMajorMinor(configured)
if (!a || !b) return null
if (a.major !== b.major) return 'major'
if (a.minor !== b.minor) return 'minor'
return null
}

export function getExportPresetsPath(): string {
// Get the preset options from any export_presets.cfg if found
const cwd = process.cwd()
Expand Down
31 changes: 31 additions & 0 deletions src/utils/ship/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {v4 as uuid} from 'uuid'

import {getNewUploadTicket, getProject, startJobsFromUpload} from '@cli/api/index.js'
import type {Job, Platform, ProjectConfig, ShipGameFlags, UploadDetails, UploadTicket} from '@cli/types'
import {detectGodotVersion, getGodotVersionDrift} from '@cli/utils/godot.js'
import {getCWDGitInfo, getFileHash} from '@cli/utils/index.js'

import {getFilesToShip} from './glob.js'
Expand All @@ -14,6 +15,26 @@ import {createZip} from './zip.js'

const ERR_NOT_CONFIGURED = 'No Android or iOS configuration found. Please run `shipthis game wizard android` or `shipthis game wizard ios` to configure your game.'

const getVersionMismatch = (detected: string, configured: string) =>
`Your project.godot targets Godot ${detected}, but this game builds with Godot ${configured}.`

// The commands go on their own lines so they stand out - this.error() and console.warn
// both preserve newlines.
const getVersionFixHint = (detected: string) =>
`To build with Godot ${detected}, update the game:\n\n` +
` shipthis game details --gameEngineVersion ${detected} --force\n\n` +
`Or ship once without changing the game:\n\n` +
` shipthis game ship --gameEngineVersion ${detected}`

const getMajorDriftError = (detected: string, configured: string) =>
`${getVersionMismatch(detected, configured)}\n` +
`Building a Godot ${detected} project with Godot ${configured} does not produce a working game, ` +
`so this ship has been stopped before building.\n\n` +
getVersionFixHint(detected)

const getMinorDriftWarning = (detected: string, configured: string) =>
`${getVersionMismatch(detected, configured)}\n\n` + getVersionFixHint(detected)

// Main function to ship the game
export async function ship({command, log, warnLog, shipFlags}: ShipOptions): Promise<Job[]> {
const commandFlags = command.getFlags() as ShipGameFlags
Expand All @@ -34,6 +55,16 @@ export async function ship({command, log, warnLog, shipFlags}: ShipOptions): Pro
if (!projectConfig.project) throw new Error('No project found in project config')
const project = await getProject(projectConfig.project.id)

// gameEngineVersion is detected once at create time and drifts when the project is
// upgraded. A major drift does not build correctly, so stop before we publish one.
if (!finalFlags.gameEngineVersion) {
const detected = detectGodotVersion()
const configured = project.details?.gameEngineVersion
const drift = detected && configured ? getGodotVersionDrift(detected, configured) : null
if (drift === 'major') throw new Error(getMajorDriftError(detected!, configured!))
if (drift === 'minor') warnLog(getMinorDriftWarning(detected!, configured!))
}

const projectUsesDemoCredentials = Boolean(project.details?.useDemoCredentials)
const isUsingDemoCredentials = useDemoCredentials ?? projectUsesDemoCredentials ?? false

Expand Down
59 changes: 58 additions & 1 deletion test/utils/godot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
import {parseEntitlementsAdditional} from '../../src/apple/entitlements.js'
import {CapabilityType} from '../../src/apple/expo.js'
import {Platform} from '../../src/types/index.js'
import {getGodotProjectCapabilities, getGodotVersion} from '../../src/utils/godot.js'
import {
detectGodotVersion,
getGodotProjectCapabilities,
getGodotVersion,
getGodotVersionDrift,
} from '../../src/utils/godot.js'

describe('getGodotProjectCapabilities', () => {
it('returns PUSH_NOTIFICATIONS when entitlements/push_notifications is Production', async () => {
Expand Down Expand Up @@ -141,6 +146,58 @@ describe('getGodotVersion', () => {
})
})

describe('detectGodotVersion', () => {
const originalCwd = process.cwd()

afterEach(() => {
process.chdir(originalCwd)
})

it('returns 3.6 for a Godot 3.x project without config/features', () => {
process.chdir(path.resolve(__dirname, '../fixtures/godot/v3_5'))
expect(detectGodotVersion()).to.equal('3.6')
})

it('returns 4.2 for a Godot 4.2 project with config/features', () => {
process.chdir(path.resolve(__dirname, '../fixtures/godot/v4_2'))
expect(detectGodotVersion()).to.equal('4.2')
})

it('returns null when there is no project.godot', () => {
process.chdir(path.resolve(__dirname, '../fixtures'))
expect(detectGodotVersion()).to.equal(null)
})
})

describe('getGodotVersionDrift', () => {
it('returns major when the majors differ', () => {
expect(getGodotVersionDrift('4.2', '3.6')).to.equal('major')
})

it('returns major for a downgrade, which is equally broken', () => {
expect(getGodotVersionDrift('3.6', '4.2')).to.equal('major')
})

it('returns minor when only the minors differ', () => {
expect(getGodotVersionDrift('4.7', '4.6')).to.equal('minor')
})

it('returns null when only the patch differs', () => {
expect(getGodotVersionDrift('4.5', '4.5.1')).to.equal(null)
})

it('returns null when a version has no minor', () => {
expect(getGodotVersionDrift('4', '4.7')).to.equal(null)
})

it('returns null for empty or garbage input', () => {
expect(getGodotVersionDrift('', '4.7')).to.equal(null)
expect(getGodotVersionDrift('4.7', '')).to.equal(null)
expect(getGodotVersionDrift('not-a-version', '4.7')).to.equal(null)
expect(getGodotVersionDrift('4.7', 'latest')).to.equal(null)
})
})

describe('parseEntitlementsAdditional', () => {
it('returns APPLE_ID_AUTH for content containing com.apple.developer.applesignin', () => {
const raw = '<key>com.apple.developer.applesignin</key><array><string>Default</string></array>'
Expand Down