feat: support field_table data in link supplement (#3008)

This commit is contained in:
Eric Seidel
2025-03-26 08:44:53 -07:00
committed by GitHub
parent 9c7ed25b79
commit 489f7be55a
10 changed files with 162 additions and 239 deletions
+1 -1
View File
@@ -1 +1 @@
3f9cefb45389b72ff073ddf305fe0939f822143b
b14323b05f28b2c72b34f57c8059ec3001d65712
+1
View File
@@ -35,6 +35,7 @@ words:
- dylib
- endtemplate
- Entra
- eseidel
- exitcode
- exportoptions
- felangel
@@ -43,20 +43,6 @@ class IosFrameworkPatcher extends Patcher {
String get _aotOutputPath =>
p.join(shorebirdEnv.buildDirectory.path, 'out.aot');
String get _patchClassTableLinkInfoFile => p.join(
shorebirdEnv.buildDirectory.path,
'ios',
'shorebird',
'App.ct.link',
);
String get _patchClassTableLinkDebugInfoPath => p.join(
shorebirdEnv.buildDirectory.path,
'ios',
'shorebird',
'App.class_table.json',
);
String get _vmcodeOutputPath =>
p.join(shorebirdEnv.buildDirectory.path, 'out.vmcode');
@@ -161,27 +147,12 @@ class IosFrameworkPatcher extends Patcher {
releaseXcframeworkPath = tempDir.path;
}
File? releaseClassTableLinkInfoFile;
File? releaseClassTableLinkDebugInfoFile;
final releaseSupplementDir = Directory.systemTemp.createTempSync();
if (supplementArtifact != null) {
final tempDir = Directory.systemTemp.createTempSync();
await artifactManager.extractZip(
zipFile: supplementArtifact,
outputDirectory: tempDir,
outputDirectory: releaseSupplementDir,
);
releaseClassTableLinkInfoFile = File(p.join(tempDir.path, 'App.ct.link'));
if (!releaseClassTableLinkInfoFile.existsSync()) {
logger.err('Unable to find class table link info file');
throw ProcessExit(ExitCode.software.code);
}
releaseClassTableLinkDebugInfoFile = File(
p.join(tempDir.path, 'App.class_table.json'),
);
if (!releaseClassTableLinkDebugInfoFile.existsSync()) {
logger.err('Unable to find class table link debug info file');
throw ProcessExit(ExitCode.software.code);
}
}
unzipProgress.complete(
@@ -194,30 +165,15 @@ class IosFrameworkPatcher extends Patcher {
final aotSnapshotFile = File(
p.join(shorebirdEnv.getShorebirdProjectRoot()!.path, 'build', 'out.aot'),
);
// TODO(eseidel): Drop support for builds before the linker.
final useLinker = AotTools.usesLinker(shorebirdEnv.flutterRevision);
if (useLinker) {
// If we're using a newer version of the linker, we need to also copy the
// necessary class table link information alongside the snapshots.
if (releaseClassTableLinkInfoFile != null &&
releaseClassTableLinkDebugInfoFile != null) {
// Copy the release's class table link info file next to the release
// snapshot so that it can be used to generate a patch.
releaseClassTableLinkInfoFile.copySync(
p.join(releaseArtifactFile.parent.path, 'App.ct.link'),
);
releaseClassTableLinkDebugInfoFile.copySync(
p.join(releaseArtifactFile.parent.path, 'App.class_table.json'),
);
// Copy the patch's class table link info file to the build directory
// so that it can be used to generate a patch.
File(
_patchClassTableLinkInfoFile,
).copySync(p.join(shorebirdEnv.buildDirectory.path, 'out.ct.link'));
File(_patchClassTableLinkDebugInfoPath).copySync(
p.join(shorebirdEnv.buildDirectory.path, 'out.class_table.json'),
);
}
apple.copySupplementFilesToSnapshotDirs(
releaseSupplementDir: releaseSupplementDir,
releaseSnapshotDir: releaseArtifactFile.parent,
patchSupplementDir: shorebirdEnv.iosSupplementDirectory,
patchSnapshotDir: shorebirdEnv.buildDirectory,
);
await apple.runLinker(
kernelFile: File(_appDillCopyPath),
@@ -43,20 +43,6 @@ class IosPatcher extends Patcher {
required super.target,
});
String get _patchClassTableLinkInfoPath => p.join(
shorebirdEnv.buildDirectory.path,
'ios',
'shorebird',
'App.ct.link',
);
String get _patchClassTableLinkDebugInfoPath => p.join(
shorebirdEnv.buildDirectory.path,
'ios',
'shorebird',
'App.class_table.json',
);
String get _aotOutputPath =>
p.join(shorebirdEnv.buildDirectory.path, 'out.aot');
@@ -243,27 +229,12 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''');
releaseXcarchivePath = tempDir.path;
}
File? releaseClassTableLinkInfoFile;
File? releaseClassTableLinkDebugInfoFile;
final releaseSupplementDir = Directory.systemTemp.createTempSync();
if (supplementArtifact != null) {
final tempDir = Directory.systemTemp.createTempSync();
await artifactManager.extractZip(
zipFile: supplementArtifact,
outputDirectory: tempDir,
outputDirectory: releaseSupplementDir,
);
releaseClassTableLinkInfoFile = File(p.join(tempDir.path, 'App.ct.link'));
if (!releaseClassTableLinkInfoFile.existsSync()) {
logger.err('Unable to find class table link info file');
throw ProcessExit(ExitCode.software.code);
}
releaseClassTableLinkDebugInfoFile = File(
p.join(tempDir.path, 'App.class_table.json'),
);
if (!releaseClassTableLinkDebugInfoFile.existsSync()) {
logger.err('Unable to find class table link debug info file');
throw ProcessExit(ExitCode.software.code);
}
}
unzipProgress.complete();
@@ -280,28 +251,12 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''');
final useLinker = AotTools.usesLinker(shorebirdEnv.flutterRevision);
if (useLinker) {
// If we're using a newer version of the linker, we need to also copy the
// necessary class table link information alongside the snapshots.
if (releaseClassTableLinkInfoFile != null &&
releaseClassTableLinkDebugInfoFile != null) {
// Copy the release's class table link info file next to the release
// snapshot so that it can be used to generate a patch.
releaseClassTableLinkInfoFile.copySync(
p.join(releaseArtifactFile.parent.path, 'App.ct.link'),
);
releaseClassTableLinkDebugInfoFile.copySync(
p.join(releaseArtifactFile.parent.path, 'App.class_table.json'),
);
// Copy the patch's class table link info file to the build directory
// so that it can be used to generate a patch.
File(
_patchClassTableLinkInfoPath,
).copySync(p.join(shorebirdEnv.buildDirectory.path, 'out.ct.link'));
File(_patchClassTableLinkDebugInfoPath).copySync(
p.join(shorebirdEnv.buildDirectory.path, 'out.class_table.json'),
);
}
apple.copySupplementFilesToSnapshotDirs(
releaseSupplementDir: releaseSupplementDir,
releaseSnapshotDir: releaseArtifactFile.parent,
patchSupplementDir: shorebirdEnv.iosSupplementDirectory,
patchSnapshotDir: shorebirdEnv.buildDirectory,
);
final (:exitCode, :linkPercentage) = await apple.runLinker(
kernelFile: File(_appDillCopyPath),
@@ -110,6 +110,60 @@ Apple get apple => read(appleRef);
/// A class that provides information about the iOS platform.
class Apple {
/// Copies the supplement files into the build directory.
/// Currently we run gen_snapshot from `flutter`, both for the release and
/// patch builds. Both times it produces supplement files in a directory.
/// In the release case, these files are zipped up and stored as an artifact
/// on our servers for later use. In the patch case, they were created on
/// disk just before this call.
/// In both cases we need to copy the supplement files from these directories
/// to right next to where the snapshot files are before calling into
/// `aot_tools` to link the two snapshots together.
// TODO(eseidel): We should pass the entire supplement directories to
// `aot_tools` rather than having to know the contents within `shorebird`.
void copySupplementFilesToSnapshotDirs({
required Directory releaseSupplementDir,
required Directory releaseSnapshotDir,
required Directory patchSupplementDir,
required Directory patchSnapshotDir,
}) {
// All known supplement files names seen across all Flutter versions.
final supplementFileNames = <String>[
'App.ct.link',
'App.class_table.json',
'App.ft.link',
'App.field_table.json',
];
// This uses maybeCopy because not all versions of gen_snapshot/aot_tools
// use the same supplement files. At the `shorebird` level we don't know
// which files should be present, so we just try to copy all.
void maybeCopy(File file, Directory destDir, {String? newBaseName}) {
if (!file.existsSync()) return;
final baseName = p.basename(file.path);
final destName =
newBaseName != null
? baseName.replaceFirst('App', newBaseName)
: baseName;
file.copySync(p.join(destDir.path, destName));
}
final releaseSupplementFiles = supplementFileNames.map(
(name) => File(p.join(releaseSupplementDir.path, name)),
);
for (final file in releaseSupplementFiles) {
maybeCopy(file, releaseSnapshotDir);
}
final patchSupplementFiles = supplementFileNames.map(
(name) => File(p.join(patchSupplementDir.path, name)),
);
const patchSnapshotBaseName = 'out';
for (final file in patchSupplementFiles) {
maybeCopy(file, patchSnapshotDir, newBaseName: patchSnapshotBaseName);
}
}
/// Returns the set of flavors for the Xcode project associated with
/// [platform], if this project has that platform configured.
Set<String>? flavors({required ApplePlatform platform}) {
@@ -155,6 +209,7 @@ class Apple {
.toSet();
}
// TODO(eseidel): Move this into a "linker" class rather than Apple.
/// Runs the linking step to minimize differences between patch and release
/// and maximize code that can be executed on the CPU.
Future<LinkResult> runLinker({
@@ -98,6 +98,11 @@ class ShorebirdEnv {
return Directory(p.join(getFlutterProjectRoot()!.path, 'build'));
}
/// Where the link supplement files are stored.
// TODO(eseidel): Make this not iOS specific.
Directory get iosSupplementDirectory =>
Directory(p.join(buildDirectory.path, 'ios', 'shorebird'));
/// The `shorebird.yaml` file for this project.
File getShorebirdYamlFile({required Directory cwd}) {
return File(p.join(cwd.path, 'shorebird.yaml'));
@@ -135,6 +135,9 @@ void main() {
when(
() => shorebirdEnv.buildDirectory,
).thenReturn(Directory(p.join(projectRoot.path, 'build')));
when(() => shorebirdEnv.iosSupplementDirectory).thenReturn(
Directory(p.join(projectRoot.path, 'build', 'shorebird', 'ios')),
);
when(
() => shorebirdEnv.getShorebirdProjectRoot(),
).thenReturn(projectRoot);
@@ -803,72 +806,6 @@ void main() {
);
});
group('when class table link info is not present', () {
setUp(() {
when(
() => artifactManager.extractZip(
zipFile: supplementArtifactFile,
outputDirectory: any(named: 'outputDirectory'),
),
).thenAnswer((invocation) async {});
});
test('exits with code 70', () async {
await expectLater(
() => runWithOverrides(
() => patcher.createPatchArtifacts(
appId: appId,
releaseId: releaseId,
releaseArtifact: releaseArtifactFile,
supplementArtifact: supplementArtifactFile,
),
),
exitsWithCode(ExitCode.software),
);
verify(
() => logger.err('Unable to find class table link info file'),
).called(1);
});
});
group('when debug info is missing', () {
setUp(() {
when(
() => artifactManager.extractZip(
zipFile: supplementArtifactFile,
outputDirectory: any(named: 'outputDirectory'),
),
).thenAnswer((invocation) async {
final outDir =
invocation.namedArguments[#outputDirectory] as Directory;
File(
p.join(outDir.path, 'App.ct.link'),
).createSync(recursive: true);
});
});
test('exits with code 70', () async {
await expectLater(
() => runWithOverrides(
() => patcher.createPatchArtifacts(
appId: appId,
releaseId: releaseId,
releaseArtifact: releaseArtifactFile,
supplementArtifact: supplementArtifactFile,
),
),
exitsWithCode(ExitCode.software),
);
verify(
() => logger.err(
'Unable to find class table link debug info file',
),
).called(1);
});
});
group('when class table link info & debug info are present', () {
setUp(() {
when(
@@ -143,6 +143,9 @@ void main() {
when(
() => shorebirdEnv.buildDirectory,
).thenReturn(Directory(p.join(projectRoot.path, 'build')));
when(() => shorebirdEnv.iosSupplementDirectory).thenReturn(
Directory(p.join(projectRoot.path, 'build', 'shorebird', 'ios')),
);
when(aotTools.isLinkDebugInfoSupported).thenAnswer((_) async => false);
@@ -1126,72 +1129,6 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}'''),
);
});
group('when class table link info is not present', () {
setUp(() {
when(
() => artifactManager.extractZip(
zipFile: supplementArtifactFile,
outputDirectory: any(named: 'outputDirectory'),
),
).thenAnswer((invocation) async {});
});
test('exits with code 70', () async {
await expectLater(
() => runWithOverrides(
() => patcher.createPatchArtifacts(
appId: appId,
releaseId: releaseId,
releaseArtifact: releaseArtifactFile,
supplementArtifact: supplementArtifactFile,
),
),
exitsWithCode(ExitCode.software),
);
verify(
() => logger.err('Unable to find class table link info file'),
).called(1);
});
});
group('when debug info is missing', () {
setUp(() {
when(
() => artifactManager.extractZip(
zipFile: supplementArtifactFile,
outputDirectory: any(named: 'outputDirectory'),
),
).thenAnswer((invocation) async {
final outDir =
invocation.namedArguments[#outputDirectory] as Directory;
File(
p.join(outDir.path, 'App.ct.link'),
).createSync(recursive: true);
});
});
test('exits with code 70', () async {
await expectLater(
() => runWithOverrides(
() => patcher.createPatchArtifacts(
appId: appId,
releaseId: releaseId,
releaseArtifact: releaseArtifactFile,
supplementArtifact: supplementArtifactFile,
),
),
exitsWithCode(ExitCode.software),
);
verify(
() => logger.err(
'Unable to find class table link debug info file',
),
).called(1);
});
});
group('when class table link info & debug info are present', () {
setUp(() {
when(
@@ -92,6 +92,65 @@ To add macOS, run "flutter create . --platforms macos"''');
).thenReturn(projectRoot);
});
group('copySupplementFilesToSnapshotDirs', () {
test('copies all files next to snapshots', () {
final names = [
'App.class_table.json',
'App.field_table.json',
'App.ct.link',
'App.ft.link',
];
void createFiles(Directory dir) {
for (final name in names) {
File(p.join(dir.path, name)).createSync();
}
}
final releaseSupplementDir = Directory.systemTemp.createTempSync();
final patchSupplementDir = Directory.systemTemp.createTempSync();
createFiles(releaseSupplementDir);
createFiles(patchSupplementDir);
final releaseSnapshotDir = Directory.systemTemp.createTempSync();
final patchSnapshotDir = Directory.systemTemp.createTempSync();
apple.copySupplementFilesToSnapshotDirs(
releaseSupplementDir: releaseSupplementDir,
releaseSnapshotDir: releaseSnapshotDir,
patchSupplementDir: patchSupplementDir,
patchSnapshotDir: patchSnapshotDir,
);
expect(Directory(releaseSnapshotDir.path).listSync(), hasLength(4));
expect(Directory(patchSnapshotDir.path).listSync(), hasLength(4));
});
test('copies only some files next to snapshots', () {
final names = ['App.class_table.json', 'App.ct.link', 'ignored.txt'];
void createFiles(Directory dir) {
for (final name in names) {
File(p.join(dir.path, name)).createSync();
}
}
final releaseSupplementDir = Directory.systemTemp.createTempSync();
final patchSupplementDir = Directory.systemTemp.createTempSync();
createFiles(releaseSupplementDir);
createFiles(patchSupplementDir);
final releaseSnapshotDir = Directory.systemTemp.createTempSync();
final patchSnapshotDir = Directory.systemTemp.createTempSync();
apple.copySupplementFilesToSnapshotDirs(
releaseSupplementDir: releaseSupplementDir,
releaseSnapshotDir: releaseSnapshotDir,
patchSupplementDir: patchSupplementDir,
patchSnapshotDir: patchSnapshotDir,
);
expect(Directory(releaseSnapshotDir.path).listSync(), hasLength(2));
expect(Directory(patchSnapshotDir.path).listSync(), hasLength(2));
});
});
group('ios', () {
final schemesPath = p.join(
'ios',
@@ -240,6 +240,24 @@ void main() {
});
});
group('iosSupplementDirectory', () {
test('returns correct path', () {
final tempDir = Directory.systemTemp.createTempSync();
File(p.join(tempDir.path, 'pubspec.yaml')).createSync(recursive: true);
Directory(
p.join(tempDir.path, 'build', 'ios', 'shorebird'),
).createSync(recursive: true);
final supplementDirectory = IOOverrides.runZoned(
() => runWithOverrides(() => shorebirdEnv.iosSupplementDirectory),
getCurrentDirectory: () => tempDir,
);
expect(
supplementDirectory.path,
equals(p.join(tempDir.path, 'build', 'ios', 'shorebird')),
);
});
});
group('macosPodfileLockFile', () {
test('returns correct path', () {
final tempDir = Directory.systemTemp.createTempSync();