mirror of https://github.com/garrytan/gstack.git
fix(ios-qa): keep private touch APIs out of release apps
This commit is contained in:
parent
630e3d3a28
commit
b8491d2d18
|
|
@ -1,7 +1,7 @@
|
|||
//
|
||||
// DebugBridgeTouch.h — public Objective-C interface for in-process touch
|
||||
// synthesis. Implementation derived from KIF (https://github.com/kif-framework/KIF),
|
||||
// MIT-licensed. The minimal subset needed to deliver a real UITouch to a
|
||||
// Apache-2.0 licensed. The minimal subset needed to deliver a real UITouch to a
|
||||
// point on the key window, including SwiftUI Buttons via iOS 18+
|
||||
// _UIHitTestContext. DEBUG-only — never link in Release.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
//
|
||||
// DebugBridgeTouch.m — minimal port of KIF's in-process touch synthesis.
|
||||
// Original code: https://github.com/kif-framework/KIF — MIT-licensed
|
||||
// (Square, Inc. + KIF contributors). Adapted to a single-file, tap-only,
|
||||
// Original code: https://github.com/kif-framework/KIF — Apache-2.0
|
||||
// Copyright 2011-2016 Square, Inc. and KIF contributors.
|
||||
// Licensed under the Apache License, Version 2.0. You may obtain a copy at
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
// Adapted to a single-file, tap-only,
|
||||
// iOS 18+ aware subset for the gstack/ios-qa DebugBridge.
|
||||
//
|
||||
// Uses these private UIKit selectors (DEBUG-only; never shipped to App Store):
|
||||
|
|
@ -20,6 +23,8 @@
|
|||
#import "DebugBridgeTouch.h"
|
||||
#import <TargetConditionals.h>
|
||||
|
||||
#if DEBUG
|
||||
|
||||
#if TARGET_OS_IOS
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
|
@ -341,3 +346,5 @@ static id DBT_HitTestView(UIWindow *window, CGPoint point) {
|
|||
@end
|
||||
|
||||
#endif // TARGET_OS_IOS
|
||||
|
||||
#endif // DEBUG
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@
|
|||
// MutationBridge implementations. Depends on the other
|
||||
// two.
|
||||
//
|
||||
// The structural Release-build guard is the `.when(configuration: .debug)`
|
||||
// conditional on every consuming target's dependency. SwiftPM refuses to link
|
||||
// DebugBridge* in Release config.
|
||||
// The structural Release-build guard is compile-time: every Swift source is
|
||||
// wrapped in `#if DEBUG`, and the Objective-C touch target receives the same
|
||||
// DEBUG define so its private-API implementation compiles to an empty object
|
||||
// in Release.
|
||||
//
|
||||
// CI invariant: `swift build -c release` + `nm -j build/Release/<binary>
|
||||
// | grep -q DebugBridge && exit 1`.
|
||||
|
|
@ -43,6 +44,9 @@ let package = Package(
|
|||
dependencies: [],
|
||||
path: "Sources/DebugBridgeTouch",
|
||||
publicHeadersPath: "include",
|
||||
cSettings: [
|
||||
.define("DEBUG", .when(configuration: .debug)),
|
||||
],
|
||||
linkerSettings: [
|
||||
// IOKit is loaded dynamically via dlopen at runtime (it's a
|
||||
// private framework on iOS and can't be linked statically).
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
// DebugBridgeCore (Foundation+Network) builds cross-platform.
|
||||
// DebugBridgeUI (UIKit/SwiftUI) is iOS-only.
|
||||
// DebugBridgeTouch (Objective-C) is iOS-only — in-process tap synthesis
|
||||
// derived from KIF (MIT). DEBUG-only.
|
||||
// derived from KIF (Apache-2.0). DEBUG-only.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
|
|
@ -32,6 +32,9 @@ let package = Package(
|
|||
dependencies: [],
|
||||
path: "Sources/DebugBridgeTouch",
|
||||
publicHeadersPath: "include",
|
||||
cSettings: [
|
||||
.define("DEBUG", .when(configuration: .debug)),
|
||||
],
|
||||
linkerSettings: [
|
||||
.linkedFramework("UIKit", .when(platforms: [.iOS])),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
//
|
||||
// DebugBridgeTouch.m — minimal port of KIF's in-process touch synthesis.
|
||||
// Original code: https://github.com/kif-framework/KIF — MIT-licensed
|
||||
// (Square, Inc. + KIF contributors). Adapted to a single-file, tap-only,
|
||||
// Original code: https://github.com/kif-framework/KIF — Apache-2.0
|
||||
// Copyright 2011-2016 Square, Inc. and KIF contributors.
|
||||
// Licensed under the Apache License, Version 2.0. You may obtain a copy at
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
// Adapted to a single-file, tap-only,
|
||||
// iOS 18+ aware subset for the gstack/ios-qa DebugBridge.
|
||||
//
|
||||
// Uses these private UIKit selectors (DEBUG-only; never shipped to App Store):
|
||||
|
|
@ -20,6 +23,8 @@
|
|||
#import "DebugBridgeTouch.h"
|
||||
#import <TargetConditionals.h>
|
||||
|
||||
#if DEBUG
|
||||
|
||||
#if TARGET_OS_IOS
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
|
@ -299,3 +304,5 @@ static id DBT_HitTestView(UIWindow *window, CGPoint point) {
|
|||
@end
|
||||
|
||||
#endif // TARGET_OS_IOS
|
||||
|
||||
#endif // DEBUG
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//
|
||||
// DebugBridgeTouch.h — public Objective-C interface for in-process touch
|
||||
// synthesis. Implementation derived from KIF (https://github.com/kif-framework/KIF),
|
||||
// MIT-licensed. The minimal subset needed to deliver a real UITouch to a
|
||||
// Apache-2.0 licensed. The minimal subset needed to deliver a real UITouch to a
|
||||
// point on the key window, including SwiftUI Buttons via iOS 18+
|
||||
// _UIHitTestContext. DEBUG-only — never link in Release.
|
||||
|
||||
|
|
|
|||
|
|
@ -19,36 +19,15 @@
|
|||
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { spawnSync } from 'child_process';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
const ROOT = join(import.meta.dir, '..');
|
||||
const FIXTURE_PATH = join(ROOT, 'test/fixtures/ios-qa/FixtureApp');
|
||||
const TEMPLATES_PATH = join(ROOT, 'ios-qa/templates');
|
||||
|
||||
// Parity: canonical Obj-C touch templates must match the fixture's working
|
||||
// copy. The fixture is the only place the .m / .h are exercised end-to-end
|
||||
// on a real device, so any divergence means consuming apps would ship a
|
||||
// stale, untested version of the SwiftUI hit-test fix.
|
||||
describe('template ↔ fixture parity', () => {
|
||||
test('DebugBridgeTouch.h.template matches fixture include', () => {
|
||||
const tmpl = readFileSync(join(TEMPLATES_PATH, 'DebugBridgeTouch.h.template'), 'utf-8');
|
||||
const fixture = readFileSync(
|
||||
join(FIXTURE_PATH, 'Sources/DebugBridgeTouch/include/DebugBridgeTouch.h'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(tmpl).toBe(fixture);
|
||||
});
|
||||
|
||||
test('DebugBridgeTouch.m.template matches fixture .m', () => {
|
||||
const tmpl = readFileSync(join(TEMPLATES_PATH, 'DebugBridgeTouch.m.template'), 'utf-8');
|
||||
const fixture = readFileSync(
|
||||
join(FIXTURE_PATH, 'Sources/DebugBridgeTouch/DebugBridgeTouch.m'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(tmpl).toBe(fixture);
|
||||
});
|
||||
|
||||
test('Package.swift.template declares all 3 DebugBridge targets', () => {
|
||||
const tmpl = readFileSync(join(TEMPLATES_PATH, 'Package.swift.template'), 'utf-8');
|
||||
// Each target must be present as a library product AND a target definition.
|
||||
|
|
@ -59,6 +38,13 @@ describe('template ↔ fixture parity', () => {
|
|||
// app gets the transitive set with one dependency entry.
|
||||
expect(tmpl).toMatch(/name:\s*"DebugBridgeUI"[\s\S]*?dependencies:\s*\["DebugBridgeCore",\s*"DebugBridgeTouch"\]/);
|
||||
});
|
||||
|
||||
test('KIF-derived touch source preserves Apache-2.0 attribution', () => {
|
||||
const implementation = readFileSync(join(TEMPLATES_PATH, 'DebugBridgeTouch.m.template'), 'utf-8');
|
||||
expect(implementation).toContain('Copyright 2011-2016 Square, Inc. and KIF contributors.');
|
||||
expect(implementation).toContain('Apache License, Version 2.0');
|
||||
expect(implementation).not.toContain('MIT-licensed');
|
||||
});
|
||||
});
|
||||
|
||||
function hasSwift(): boolean {
|
||||
|
|
@ -151,4 +137,49 @@ describeIfSwift('swift build invariants', () => {
|
|||
}
|
||||
expect(foundForbidden).toBe(0);
|
||||
}, 300_000);
|
||||
|
||||
test('final Release iOS app excludes private touch APIs', () => {
|
||||
if (spawnSync('xcrun', ['--find', 'xcodebuild']).status !== 0 ||
|
||||
spawnSync('xcodegen', ['--version']).status !== 0) return;
|
||||
|
||||
const workDir = mkdtempSync(join(tmpdir(), 'gstack-ios-release-guard-'));
|
||||
const fixtureDir = join(workDir, 'FixtureApp');
|
||||
try {
|
||||
cpSync(FIXTURE_PATH, fixtureDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(fixtureDir, 'Package.swift'),
|
||||
readFileSync(join(TEMPLATES_PATH, 'Package.swift.template'), 'utf-8'),
|
||||
);
|
||||
writeFileSync(
|
||||
join(fixtureDir, 'Sources/DebugBridgeTouch/DebugBridgeTouch.m'),
|
||||
readFileSync(join(TEMPLATES_PATH, 'DebugBridgeTouch.m.template'), 'utf-8'),
|
||||
);
|
||||
writeFileSync(
|
||||
join(fixtureDir, 'Sources/DebugBridgeTouch/include/DebugBridgeTouch.h'),
|
||||
readFileSync(join(TEMPLATES_PATH, 'DebugBridgeTouch.h.template'), 'utf-8'),
|
||||
);
|
||||
expect(spawnSync('xcodegen', [
|
||||
'generate', '--spec', join(fixtureDir, 'project.yml'),
|
||||
'--project', fixtureDir, '--project-root', fixtureDir,
|
||||
], { cwd: fixtureDir, stdio: 'pipe' }).status).toBe(0);
|
||||
|
||||
const derivedData = join(workDir, 'DerivedData');
|
||||
const build = spawnSync('xcodebuild', [
|
||||
'-project', join(fixtureDir, 'FixtureApp.xcodeproj'),
|
||||
'-scheme', 'FixtureApp', '-configuration', 'Release',
|
||||
'-sdk', 'iphonesimulator', '-destination', 'generic/platform=iOS Simulator',
|
||||
'-derivedDataPath', derivedData, 'CODE_SIGNING_ALLOWED=NO', 'build',
|
||||
], { cwd: fixtureDir, stdio: 'pipe', timeout: 300_000 });
|
||||
expect(build.status).toBe(0);
|
||||
|
||||
const binary = join(derivedData, 'Build/Products/Release-iphonesimulator/FixtureApp.app/FixtureApp');
|
||||
const contents = spawnSync('strings', ['-a', binary]).stdout?.toString() ?? '';
|
||||
for (const forbidden of [
|
||||
'DebugBridgeTouch', '_touchesEvent', '_AXSSetAutomationEnabled',
|
||||
'IOHIDEventCreateDigitizerEvent',
|
||||
]) expect(contents).not.toContain(forbidden);
|
||||
} finally {
|
||||
rmSync(workDir, { recursive: true, force: true });
|
||||
}
|
||||
}, 360_000);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue