react-native-unity
react-native-unity copied to clipboard
Utility scripts for automating the integration of updates to unity artifacts
In my project we're regularly publishing new versions of a unity game to incorporate into our RN app. In the documentation steps for integration, 4 and 5 for iOS and 6 for Android need to be done every time. I created a script to make these changes automatically and thought it might be useful to share! It's not really a change that can be PR'd and it seems to long to just add to the README, so I'm just creating this issue and letting the owner decide if anything should be done with it.
Android is a simple regex to remove the intent filter line, but for iOS I used this stack overflow recommendation to convert the Unity project.pbxproj file to JSON for easy editing, modify it, and save the updates. This does mean the output file is in JSON rather than the original ASCII property list format, but XCode has no problems reading it.
I used the package minimist to read in args from the command line to pass in the path to the manifest/project.pbxproj files, but that can be swapped out by getting input args via process.argv or hardcoding it. I also used typescript, but the types can be taken out and replace ts-node with node and it'll all work fine.
Android:
import fs from "fs";
import minimist from "minimist";
const { file: MANIFEST_PATH } = minimist(process.argv.slice(2));
if (!MANIFEST_PATH) {
throw new Error("Missing android manifest path", MANIFEST_PATH);
}
if (!fs.existsSync(MANIFEST_PATH)) {
throw new Error("Invalid manifest path path", MANIFEST_PATH);
}
const androidManifest = fs.readFileSync(MANIFEST_PATH).toString();
// Want to remove everything between <intent-filter> and </intent-filter>
const updatedManifest = androidManifest
.split(/.+<intent-filter>[\s\S]+<\/intent-filter>\n/)
.join("");
fs.writeFileSync(MANIFEST_PATH, updatedManifest);
call it passing in ts-node updateManifest.ts --MANIFEST_PATH "[path_to_manifest]
iOS:
import crypto from "crypto";
import fs from "fs";
import { execSync } from "child_process";
import minimist from "minimist";
// Not a complete interface, but defining the parts that are needed for this script
interface Pbxproj {
objects: {
[key: string]: PbxprojObject;
};
}
interface PbxprojObject {
buildPhases?: string[];
fileRef?: string;
files?: string[];
isa?: string;
lastKnownFileType?: string;
name?: string;
path?: string;
productName?: string;
productReference?: string;
settings?: {
ATTRIBUTES: Array<
"Public" | "Weak" | "CodeSignOnCopy" | "RemoveHeadersOnCopy"
>;
};
sourceTree?: string;
}
const { file: PBXPROJ_PATH } = minimist(process.argv.slice(2));
if (!PBXPROJ_PATH) {
throw new Error("Missing pbxproj path", PBXPROJ_PATH);
}
if (!fs.existsSync(PBXPROJ_PATH)) {
throw new Error("Invalid pbxproj path", PBXPROJ_PATH);
}
// Generates a UUID of the kind expected for file ids in the pbxproj file
const createPbxprojUuid = (): string => {
return crypto.randomUUID().replaceAll("-", "").toUpperCase().slice(0, 24);
};
// Loops through the objects and finds an object that matches a key-value pair
const findObjectIdByValue = (
pbxproj: Pbxproj,
key: keyof PbxprojObject,
matchingValue: string
): string => {
const matchingObject = Object.entries(pbxproj.objects).find(
([_, value]) => value?.[key] === matchingValue
)?.[0];
if (!matchingObject) {
throw new Error(
`Fatal Error: Could not find match for key ${key} and value ${matchingValue}`
);
}
return matchingObject;
};
const findAllObjectIdsByValue = (
pbxproj: Pbxproj,
key: keyof PbxprojObject,
matchingValue: string
): string[] => {
const matchingObjects = Object.entries(pbxproj.objects)
.filter(([_, value]) => value?.[key] === matchingValue)
.map((obj) => obj?.[0]);
if (!matchingObjects) {
throw new Error(
`Fatal Error: Could not find match for key ${key} and value ${matchingValue}`
);
}
return matchingObjects;
};
execSync(`plutil -convert json ${PBXPROJ_PATH}`);
const pbxproj: Pbxproj = JSON.parse(fs.readFileSync(PBXPROJ_PATH).toString());
if (!pbxproj.objects) {
throw new Error(`No valid pbxproj file found at ${PBXPROJ_PATH}`);
}
/* CHANGE: Creating a new ID reference for the Data folder, and add to the UnityFramework project */
// Getting the existing UUID of the data object to reference
const dataFileRef = findObjectIdByValue(pbxproj, "path", "Data");
// Creating a new UUID to use to reference the data object inside UnityFramework
const newDataUUID = createPbxprojUuid();
pbxproj.objects[newDataUUID] = {
isa: "PBXBuildFile",
fileRef: dataFileRef,
};
const unityFrameworkId = findObjectIdByValue(
pbxproj,
"productName",
"UnityFramework"
);
// The most fragile part of this script, the Resources tag is the only one with an isa value of PBXResourcesBuildPhase that is in the unityFrameworkBuildPhases
const unityFrameworkBuildPhases =
pbxproj.objects[unityFrameworkId]?.buildPhases;
if (!unityFrameworkBuildPhases) {
throw new Error("Unable to find unity build phases");
}
const potentialFrameworkResourceIds = findAllObjectIdsByValue(
pbxproj,
"isa",
"PBXResourcesBuildPhase"
);
const unityFrameworkResourcesId = potentialFrameworkResourceIds.find((id) =>
unityFrameworkBuildPhases.includes(id)
);
if (!unityFrameworkResourcesId) {
throw new Error("Unable to find unity framework resources id");
}
pbxproj.objects[unityFrameworkResourcesId]?.files?.push(newDataUUID);
/* CHANGE: Set NativeCallProxy.h file to have the public attribute */
const nativeCallProxyFileRef = findObjectIdByValue(
pbxproj,
"name",
"NativeCallProxy.h"
);
const nativeCallProxyHeaderId = findObjectIdByValue(
pbxproj,
"fileRef",
nativeCallProxyFileRef
);
// Above function throws an error if ID isn't in object
(pbxproj.objects[nativeCallProxyHeaderId] as PbxprojObject).settings = {
ATTRIBUTES: ["Public"],
};
/* CHANGE: Set unity target app object to have a modified name and path variable */
const unityProjectID = findObjectIdByValue(pbxproj, "name", "Unity-iPhone");
const unityProductReference = pbxproj.objects[unityProjectID]?.productReference;
if (!unityProductReference || !pbxproj.objects[unityProductReference]) {
throw new Error("Unable to find unity target app object");
}
// These are guaranteed to exist by the above check
(pbxproj.objects[unityProductReference] as PbxprojObject).path =
"Tree Valley.app";
(pbxproj.objects[unityProductReference] as PbxprojObject).name =
"Unity-Target-New.app";
fs.writeFileSync(PBXPROJ_PATH, JSON.stringify(pbxproj));
@jayfeldman12 thanks for sharing!