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
|
// DebugBridgeTouch.h — public Objective-C interface for in-process touch
|
||||||
// synthesis. Implementation derived from KIF (https://github.com/kif-framework/KIF),
|
// 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+
|
// point on the key window, including SwiftUI Buttons via iOS 18+
|
||||||
// _UIHitTestContext. DEBUG-only — never link in Release.
|
// _UIHitTestContext. DEBUG-only — never link in Release.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
//
|
//
|
||||||
// DebugBridgeTouch.m — minimal port of KIF's in-process touch synthesis.
|
// DebugBridgeTouch.m — minimal port of KIF's in-process touch synthesis.
|
||||||
// Original code: https://github.com/kif-framework/KIF — MIT-licensed
|
// Original code: https://github.com/kif-framework/KIF — Apache-2.0
|
||||||
// (Square, Inc. + KIF contributors). Adapted to a single-file, tap-only,
|
// 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.
|
// iOS 18+ aware subset for the gstack/ios-qa DebugBridge.
|
||||||
//
|
//
|
||||||
// Uses these private UIKit selectors (DEBUG-only; never shipped to App Store):
|
// Uses these private UIKit selectors (DEBUG-only; never shipped to App Store):
|
||||||
|
|
@ -20,6 +23,8 @@
|
||||||
#import "DebugBridgeTouch.h"
|
#import "DebugBridgeTouch.h"
|
||||||
#import <TargetConditionals.h>
|
#import <TargetConditionals.h>
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
|
||||||
#if TARGET_OS_IOS
|
#if TARGET_OS_IOS
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
|
@ -341,3 +346,5 @@ static id DBT_HitTestView(UIWindow *window, CGPoint point) {
|
||||||
@end
|
@end
|
||||||
|
|
||||||
#endif // TARGET_OS_IOS
|
#endif // TARGET_OS_IOS
|
||||||
|
|
||||||
|
#endif // DEBUG
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,10 @@
|
||||||
// MutationBridge implementations. Depends on the other
|
// MutationBridge implementations. Depends on the other
|
||||||
// two.
|
// two.
|
||||||
//
|
//
|
||||||
// The structural Release-build guard is the `.when(configuration: .debug)`
|
// The structural Release-build guard is compile-time: every Swift source is
|
||||||
// conditional on every consuming target's dependency. SwiftPM refuses to link
|
// wrapped in `#if DEBUG`, and the Objective-C touch target receives the same
|
||||||
// DebugBridge* in Release config.
|
// 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>
|
// CI invariant: `swift build -c release` + `nm -j build/Release/<binary>
|
||||||
// | grep -q DebugBridge && exit 1`.
|
// | grep -q DebugBridge && exit 1`.
|
||||||
|
|
@ -43,6 +44,9 @@ let package = Package(
|
||||||
dependencies: [],
|
dependencies: [],
|
||||||
path: "Sources/DebugBridgeTouch",
|
path: "Sources/DebugBridgeTouch",
|
||||||
publicHeadersPath: "include",
|
publicHeadersPath: "include",
|
||||||
|
cSettings: [
|
||||||
|
.define("DEBUG", .when(configuration: .debug)),
|
||||||
|
],
|
||||||
linkerSettings: [
|
linkerSettings: [
|
||||||
// IOKit is loaded dynamically via dlopen at runtime (it's a
|
// IOKit is loaded dynamically via dlopen at runtime (it's a
|
||||||
// private framework on iOS and can't be linked statically).
|
// private framework on iOS and can't be linked statically).
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
// DebugBridgeCore (Foundation+Network) builds cross-platform.
|
// DebugBridgeCore (Foundation+Network) builds cross-platform.
|
||||||
// DebugBridgeUI (UIKit/SwiftUI) is iOS-only.
|
// DebugBridgeUI (UIKit/SwiftUI) is iOS-only.
|
||||||
// DebugBridgeTouch (Objective-C) is iOS-only — in-process tap synthesis
|
// 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
|
import PackageDescription
|
||||||
|
|
||||||
|
|
@ -32,6 +32,9 @@ let package = Package(
|
||||||
dependencies: [],
|
dependencies: [],
|
||||||
path: "Sources/DebugBridgeTouch",
|
path: "Sources/DebugBridgeTouch",
|
||||||
publicHeadersPath: "include",
|
publicHeadersPath: "include",
|
||||||
|
cSettings: [
|
||||||
|
.define("DEBUG", .when(configuration: .debug)),
|
||||||
|
],
|
||||||
linkerSettings: [
|
linkerSettings: [
|
||||||
.linkedFramework("UIKit", .when(platforms: [.iOS])),
|
.linkedFramework("UIKit", .when(platforms: [.iOS])),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
//
|
//
|
||||||
// DebugBridgeTouch.m — minimal port of KIF's in-process touch synthesis.
|
// DebugBridgeTouch.m — minimal port of KIF's in-process touch synthesis.
|
||||||
// Original code: https://github.com/kif-framework/KIF — MIT-licensed
|
// Original code: https://github.com/kif-framework/KIF — Apache-2.0
|
||||||
// (Square, Inc. + KIF contributors). Adapted to a single-file, tap-only,
|
// 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.
|
// iOS 18+ aware subset for the gstack/ios-qa DebugBridge.
|
||||||
//
|
//
|
||||||
// Uses these private UIKit selectors (DEBUG-only; never shipped to App Store):
|
// Uses these private UIKit selectors (DEBUG-only; never shipped to App Store):
|
||||||
|
|
@ -20,6 +23,8 @@
|
||||||
#import "DebugBridgeTouch.h"
|
#import "DebugBridgeTouch.h"
|
||||||
#import <TargetConditionals.h>
|
#import <TargetConditionals.h>
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
|
||||||
#if TARGET_OS_IOS
|
#if TARGET_OS_IOS
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
|
@ -299,3 +304,5 @@ static id DBT_HitTestView(UIWindow *window, CGPoint point) {
|
||||||
@end
|
@end
|
||||||
|
|
||||||
#endif // TARGET_OS_IOS
|
#endif // TARGET_OS_IOS
|
||||||
|
|
||||||
|
#endif // DEBUG
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//
|
//
|
||||||
// DebugBridgeTouch.h — public Objective-C interface for in-process touch
|
// DebugBridgeTouch.h — public Objective-C interface for in-process touch
|
||||||
// synthesis. Implementation derived from KIF (https://github.com/kif-framework/KIF),
|
// 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+
|
// point on the key window, including SwiftUI Buttons via iOS 18+
|
||||||
// _UIHitTestContext. DEBUG-only — never link in Release.
|
// _UIHitTestContext. DEBUG-only — never link in Release.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,36 +19,15 @@
|
||||||
|
|
||||||
import { describe, test, expect } from 'bun:test';
|
import { describe, test, expect } from 'bun:test';
|
||||||
import { spawnSync } from 'child_process';
|
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';
|
import { join } from 'path';
|
||||||
|
|
||||||
const ROOT = join(import.meta.dir, '..');
|
const ROOT = join(import.meta.dir, '..');
|
||||||
const FIXTURE_PATH = join(ROOT, 'test/fixtures/ios-qa/FixtureApp');
|
const FIXTURE_PATH = join(ROOT, 'test/fixtures/ios-qa/FixtureApp');
|
||||||
const TEMPLATES_PATH = join(ROOT, 'ios-qa/templates');
|
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', () => {
|
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', () => {
|
test('Package.swift.template declares all 3 DebugBridge targets', () => {
|
||||||
const tmpl = readFileSync(join(TEMPLATES_PATH, 'Package.swift.template'), 'utf-8');
|
const tmpl = readFileSync(join(TEMPLATES_PATH, 'Package.swift.template'), 'utf-8');
|
||||||
// Each target must be present as a library product AND a target definition.
|
// 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.
|
// app gets the transitive set with one dependency entry.
|
||||||
expect(tmpl).toMatch(/name:\s*"DebugBridgeUI"[\s\S]*?dependencies:\s*\["DebugBridgeCore",\s*"DebugBridgeTouch"\]/);
|
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 {
|
function hasSwift(): boolean {
|
||||||
|
|
@ -151,4 +137,49 @@ describeIfSwift('swift build invariants', () => {
|
||||||
}
|
}
|
||||||
expect(foundForbidden).toBe(0);
|
expect(foundForbidden).toBe(0);
|
||||||
}, 300_000);
|
}, 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