Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: stop crashing from objc_getClassList #7187

Merged
merged 13 commits into from
Jan 15, 2024
16 changes: 16 additions & 0 deletions cli/src/ios/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ import {
import type { Plugin } from '../plugin';
import { copy as copyTask } from '../tasks/copy';
import { convertToUnixPath } from '../util/fs';
import {
getPluginFiles,
findPluginClasses,
writePluginJSON,
} from '../util/iosplugin';
import { resolveNode } from '../util/node';
import { checkPackageManager, generatePackageFile } from '../util/spm';
import { runCommand, isInstalled } from '../util/subprocess';
Expand All @@ -56,6 +61,8 @@ export async function updateIOS(
await updateIOSCocoaPods(config, plugins, deployment);
}

generateIOSPackageJSON(config, plugins);

printPlugins(capacitorPlugins, 'ios');
}

Expand Down Expand Up @@ -173,6 +180,15 @@ end`,
}
}

async function generateIOSPackageJSON(
config: Config,
plugins: Plugin[],
): Promise<void> {
const fileList = await getPluginFiles(plugins);
const classList = await findPluginClasses(fileList);
writePluginJSON(config, classList);
}

async function getRelativeCapacitoriOSPath(config: Config) {
const capacitoriOSPath = resolveNode(
config.app.rootDir,
Expand Down
70 changes: 70 additions & 0 deletions cli/src/util/iosplugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { ReaddirPOptions } from '@ionic/utils-fs';
import {
readFileSync,
readdirp,
readJSONSync,
writeJSONSync,
} from '@ionic/utils-fs';
import { resolve } from 'path';

import type { Config } from '../definitions';
import type { Plugin } from '../plugin';

export async function getPluginFiles(plugins: Plugin[]): Promise<string[]> {
let filenameList: string[] = [];

const options: ReaddirPOptions = {
filter: item => {
if (item.path.endsWith('.swift') || item.path.endsWith('.m')) {
return true;
} else {
return false;
}
},
};

for (const plugin of plugins) {
if (typeof plugin.ios?.name !== 'undefined') {
const pluginPath = resolve(plugin.rootPath, plugin.ios?.path);
const filenames = await readdirp(pluginPath, options);
filenameList = filenameList.concat(filenames);
}
}

return filenameList;
}

export async function findPluginClasses(files: string[]): Promise<string[]> {
const classList: string[] = [];

for (const file of files) {
const fileData = readFileSync(file, 'utf-8');
const swiftPluginRegex = RegExp(/@objc\(([A-Za-z0-9_-]+)\)/);
const objcPluginRegex = RegExp(/CAP_PLUGIN\(([A-Za-z0-9_-]+)/);

const swiftMatches = swiftPluginRegex.exec(fileData);
if (swiftMatches?.[1] != null) {
classList.push(swiftMatches[1]);
}

const objcMatches = objcPluginRegex.exec(fileData);
if (objcMatches?.[1] != null) {
classList.push(objcMatches[1]);
}
}

return classList;
}

export async function writePluginJSON(
config: Config,
classList: string[],
): Promise<void> {
const capJSONFile = resolve(
config.ios.nativeTargetDirAbs,
'capacitor.config.json',
);
const capJSON = readJSONSync(capJSONFile);
capJSON['packageClassList'] = classList;
writeJSONSync(capJSONFile, capJSON, { spaces: '\t' });
}
46 changes: 26 additions & 20 deletions ios/Capacitor/Capacitor/CapacitorBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import Cordova

internal typealias CapacitorPlugin = CAPPlugin & CAPBridgedPlugin

struct RegistrationList: Codable {
let packageClassList: [String]
}

/**
An internal class adopting a public protocol means that we have a lot of `public` methods
but that is by design not a mistake. And since the bridge is the center of the whole project
Expand Down Expand Up @@ -277,30 +281,32 @@ open class CapacitorBridge: NSObject, CAPBridgeProtocol {
Register all plugins that have been declared
*/
func registerPlugins() {
if autoRegisterPlugins {
let classCount = objc_getClassList(nil, 0)
let classes = UnsafeMutablePointer<AnyClass?>.allocate(capacity: Int(classCount))
var pluginList: [AnyClass] = [CAPHttpPlugin.self, CAPConsolePlugin.self, CAPWebViewPlugin.self, CAPCookiesPlugin.self]

let releasingClasses = AutoreleasingUnsafeMutablePointer<AnyClass>(classes)
let numClasses: Int32 = objc_getClassList(releasingClasses, classCount)

for classIndex in 0..<Int(numClasses) {
if let aClass: AnyClass = classes[classIndex] {
if class_getSuperclass(aClass) == CDVPlugin.self {
injectCordovaFiles = true
}
if class_conformsToProtocol(aClass, CAPBridgedPlugin.self),
let pluginType = aClass as? CapacitorPlugin.Type {
if aClass is CAPInstancePlugin.Type { continue }
registerPlugin(pluginType)
if autoRegisterPlugins {
do {
if let pluginJSON = Bundle.main.url(forResource: "capacitor.config", withExtension: "json") {
let pluginData = try Data(contentsOf: pluginJSON)
let registrationList = try JSONDecoder().decode(RegistrationList.self, from: pluginData)
for plugin in registrationList.packageClassList {
if let pluginClass = NSClassFromString(plugin) {
if class_getSuperclass(pluginClass) == CDVPlugin.self {
injectCordovaFiles = true
}
pluginList.append(pluginClass)
}
}
}
} catch {
CAPLog.print("Error registering plugins: \(error)")
}
}

for plugin in pluginList {
if plugin is CAPInstancePlugin.Type { continue }
if let capPlugin = plugin as? CapacitorPlugin.Type {
registerPlugin(capPlugin)
}
classes.deallocate()
} else {
// register core plugins only
[CAPHttpPlugin.self, CAPConsolePlugin.self, CAPWebViewPlugin.self, CAPCookiesPlugin.self]
.forEach { registerPluginType($0) }
}
}

Expand Down