/libgdx-one-click-deployment

Gradle configuration to upload and distribute your libgdx game using testflight and testfairy

Primary LanguageGroovy

libgdx-one-click-deployment

First we include this duplicate code in the android/build.gradle and ios/build.gradle:

  • A versioning software reader:
// Read current version from properties file
ext.versionFile = file('version.properties')

task loadVersion {
    project.version = readVersion()
}

ProjectVersion readVersion() {
    logger.quiet 'Reading the android version file.'

    if (!versionFile.exists()) {
        throw new GradleException("Required version file does not exit: $versionFile.canonicalPath")
    }

    Properties versionProps = new Properties()

    versionFile.withInputStream { stream ->
        versionProps.load(stream)
    }

    new ProjectVersion(versionProps.major.toInteger(), versionProps.minor.toInteger(), versionProps.patch.toInteger(), versionProps.build.toInteger())
}
  • Auto-increment version tasks:
// Control version
task incrementMajorVersion(group: 'versioning', description: 'Increments project major version.') << {
    String currentVersion = version.toString()
    ++version.major
	// Reset minor and patch
	version.minor = 0
	version.patch = 0
    String newVersion = version.toString()
    logger.info "Incrementing major project version: $currentVersion -> $newVersion"

    ant.propertyfile(file: versionFile) {
        entry(key: 'major', type: 'int', operation: '+', value: 1)
		entry(key: 'minor', type: 'int', operation: '=', value: 0)
		entry(key: 'patch', type: 'int', operation: '=', value: 0)
    }
}

task incrementMinorVersion(group: 'versioning', description: 'Increments project minor version.') << {
    String currentVersion = version.toString()
    ++version.minor
	// Reset patch
	version.patch = 0
    String newVersion = version.toString()
    logger.info "Incrementing minor project version: $currentVersion -> $newVersion"

    ant.propertyfile(file: versionFile) {
        entry(key: 'minor', type: 'int', operation: '+', value: 1)
		entry(key: 'patch', type: 'int', operation: '=', value: 0)
    }
}

task incrementPatchVersion(group: 'versioning', description: 'Increments project patch version.') << {
    String currentVersion = version.toString()
    ++version.patch
    String newVersion = version.toString()
    logger.info "Incrementing patch project version: $currentVersion -> $newVersion"

    ant.propertyfile(file: versionFile) {
        entry(key: 'patch', type: 'int', operation: '+', value: 1)
    }
}

task incrementBuildVersion(group: 'versioning', description: 'Increments project build version.') << {
    String currentVersion = version.toString()
    ++version.build
    String newVersion = version.toString()
    logger.info "Incrementing build project version: $currentVersion -> $newVersion"

    ant.propertyfile(file: versionFile) {
        entry(key: 'build', type: 'int', operation: '+', value: 1)
    }
}
  • At the end a ProjectVersion Class Definition:
// Class definition
class ProjectVersion {
	
    Integer major
	
    Integer minor
	
    Integer patch
	
    Integer build

    ProjectVersion(Integer major, Integer minor, Integer patch, Integer build) {
        this.major = major
        this.minor = minor
        this.patch = patch
        this.build = build
    }

    String getVersionName() {
        this.major + "." + this.minor + "." + this.patch
    }
	
    String getVersionCode() {
		Integer.parseInt(new Date().format('yyyyMMdd')) + this.build
    }

    @Override
    String toString() {
        this.getVersionName() + "_" + this.getVersionCode()
    }
}

Then both in the android and ios we create two properties files called version.properties that contains this:

major=0
minor=0
patch=0
build=0

And add define global properties at the root of our project in your gradle.properties file:

# Testflight configuration
iosBuildPath=ios/build/robovm/
iosAppName=IOSLauncher
testflightURL=http://testflightapp.com/api/builds.json
testflightAT=YOUR_API_TOKEN
testflightTT=YOUR_TEAM_TOKEN
testflightDefaultNote=This build was uploaded via the upload API and Gradle
testflightNotify=True
testflightDL=YOUR_DISTRIBUTE_LIST_NAMES_SEPARATES_BY_COMMAS
# Testfairy configuration
androidBuildPath=android/build/apk/
testfairyURL=https://app.testfairy.com/api/upload
testfairyAK=YOUR_API_KEY
androidAppName=android-release
testfairyProguardPath=android/proguard-project.txt
testfairyTG=YOUR_GROUP_NAMES_SEPARATES_BY_COMMAS
testfairyMetrics=cpu,memory,network,logcat
testfairyVideo=off
testfairyMD=10m
testfairyVQ=low
testfairyVR=1.0
testfairyIW=off
testfairyComment=This build was uploaded via the upload API and Gradle

Next we configure individual tasks for android/build.gradle.

Locate project android and add a signing configuration to sign our game with default android keystore values. It should look like this:

android {
    
	// ...
	
    signingConfigs {
        release {
            storeFile file( System.getProperty("user.home") + "/.android/debug.keystore")
            storePassword "android"
            keyAlias "androiddebugkey"
            keyPassword "android"
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }

}

Next you insert a dependency before preReleaseBuild to auto-increment build version with each build.

task updateAndroidManifestXML(group: 'versioning', description: 'Updates AndroidManifest.xml project file.') << {
	
    def manifestFile = file("AndroidManifest.xml")
    
	def pattern = java.util.regex.Pattern.compile("versionCode=\"(\\d+)\"")
    def manifestContent = manifestFile.getText()
    def matcher = pattern.matcher(manifestContent)
    matcher.find()
    manifestContent = matcher.replaceFirst("versionCode=\"" + version.getVersionCode() + "\"")
	
	pattern = java.util.regex.Pattern.compile("versionName=\"(.*)\"")
    matcher = pattern.matcher(manifestContent)
    matcher.find()
    manifestContent = matcher.replaceFirst("versionName=\"" + version.getVersionName() + "\"")
    
	manifestFile.write(manifestContent)
	
}

// Autoincrement build versioning
updateAndroidManifestXML.dependsOn incrementBuildVersion

tasks.whenTaskAdded { task ->
	if( task.name == 'preReleaseBuild' )
		task.dependsOn updateAndroidManifestXML
}

And finally we include a task to upload and distribute our game using testfairy

task execTestfairyUpload (type:Exec) {
	
	def currentWorkingDir = System.getProperty("user.dir")
	commandLine 'curl'
	args testfairyURL,
		 '-F',
		 'api_key=' + testfairyAK,
		 '-F',
		 'apk_file=@' + currentWorkingDir + '/' + androidBuildPath + androidAppName + '.apk',
		 '-F',
		 'proguard_file=@' + currentWorkingDir + '/' + testfairyProguardPath,
		 '-F',
		 'testers_groups=' + testfairyTG,
		 '-F',
		 'metrics=' + testfairyMetrics,
		 '-F',
		 'max-duration=' + testfairyMD,
		 '-F',
		 'video=' + testfairyVideo,
		 '-F',
		 'video-quality=' + testfairyVQ,
		 '-F',
		 'video-rate=' + testfairyVR,
		 '-F',
		 'icon-watermark=' + testfairyIW,
		 '-F',
		 'comment=' + testfairyComment
	//store the output instead of printing to the console:
	standardOutput = new ByteArrayOutputStream()

	//extension method execTestfairyUpload.output() can be used to obtain the output:
	ext.output = {
		return standardOutput.toString()
	}
	
}

task testfairyUpload(dependsOn: execTestfairyUpload) << {
	logger.info "${execTestfairyUpload.output()}"
}

It is now the turn of ios/build.gradle. Open this and modify createIPA task so it depends on update of robovm.properties. As we shall she below updateRoboVMProperties task will update app.version and app.build. Also updateRoboVMProperties task depends on a increment build version task:

- createIPA.dependsOn build

+ // Update info.plist.xml
+ task updateRoboVMProperties << {
+ 	
+     ant.propertyfile(file: robovmFile) {
+         entry(key: 'app.version', type: 'string', operation: '=', value: version.getVersionName())
+     }
+ 	
+     ant.propertyfile(file: robovmFile) {
+         entry(key: 'app.build', type: 'int', operation: '=', value: version.build.toString())
+     }
+ 	
+ }

+ // Autoincrement build versioning
+ incrementBuildVersion.dependsOn build
+ updateRoboVMProperties.dependsOn incrementBuildVersion
+ createIPA.dependsOn updateRoboVMProperties

And also finally we include a task to zip dSYM, upload and distribute our game using testflight in this occasion

task execTestflightZip (type:Exec) {
	
	def currentWorkingDir = System.getProperty("user.dir")
	commandLine '/usr/bin/zip'
	args '-r',
		 currentWorkingDir + '/' + iosBuildPath + 'IOSLauncher.app.dSYM.zip',
		 currentWorkingDir + '/' + iosBuildPath + 'IOSLauncher.app.dSYM'
	//store the output instead of printing to the console:
	standardOutput = new ByteArrayOutputStream()

	//extension method execTestflightDistribute.output() can be used to obtain the output:
	ext.output = {
		return standardOutput.toString()
	}
	
}

task testflightZip(dependsOn: execTestflightZip) << {
	logger.info "${execTestflightZip.output()}"
}

task execTestflightUpload (type:Exec, dependsOn: testflightZip) {
	
	def currentWorkingDir = System.getProperty("user.dir")
	commandLine 'curl'
	args testflightURL,
		 '-F',
		 'file=@' + currentWorkingDir + '/' + iosBuildPath + iosAppName + '.ipa',
		 '-F',
		 'dsym=@' + currentWorkingDir + '/' + iosBuildPath + iosAppName + '.app.dSYM.zip',
		 '-F',
		 'api_token=' + testflightAT,
		 '-F',
		 'team_token=' + testflightTT,
		 '-F',
		 'notes=' + testflightDefaultNote,
		 '-F',
		 'notify=' + testflightNotify,
		 '-F',
		 'distribution_lists=' + testflightDL
	//store the output instead of printing to the console:
	standardOutput = new ByteArrayOutputStream()

	//extension method execTestflightDistribute.output() can be used to obtain the output:
	ext.output = {
		return standardOutput.toString()
	}
	
}

task testflightUpload(dependsOn: execTestflightUpload) << {
	logger.info "${execTestflightUpload.output()}"
}

Distribute anywhere

Lack of resources is a major problem that faces an studio indie.

Package and distribute tasks only need commandline so that we'll configurate our MAC to access remotely by SSH from any system, including your phone for an emergency.

There are hundreds of tutorials on how to do this. Here's an example. I'm just giving an example of the potential for our approach.

Make sure you have the Mac environment properly configured according to libGDX WIKI and here a set of examples:

ssh user@server
// First increment minor version, increment build version, create *.ipa and distribute using testflight
server:~ user$ ./gradlew ios:incrementMinorVersion ios:createIPA ios:testflightUpload
// Increment build version, create *.apk and distribute using testfairy
server:~ user$ ./gradlew android:assembleRelease android:testfairyUpload

IMPORTANT: Testers with tethered phones can't work with testflight. An alternative is distribute our *.ipa with an a cloud storage service and they install using iFunbox.