Skip to content

Commit a511bbc

Browse files
daugeldaugeSpace Team
authored and
Space Team
committed
[Apple] Required reason APIs finder script
^KT-67690
1 parent 5ae6851 commit a511bbc

File tree

2 files changed

+296
-0
lines changed

2 files changed

+296
-0
lines changed

.space/CODEOWNERS

+1
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@
359359
/libraries/tools/maven-archetypes/ "Kotlin Build Tools"
360360
/libraries/tools/mutability-annotations-compat/ "Kotlin Libraries"
361361
/libraries/tools/script-runtime/ "Kotlin Compiler Core"
362+
/libraries/tools/required-reason-finder/ "Kotlin Apple Ecosystem"
362363

363364
/libraries/maven-settings.xml "Kotlin Build Infrastructure"
364365
/libraries/pom.xml "Kotlin Build Infrastructure"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
from subprocess import DEVNULL, PIPE
2+
from shutil import copyfile
3+
from pathlib import Path
4+
from typing import List
5+
import urllib.request
6+
import subprocess
7+
import argparse
8+
import platform
9+
import tempfile
10+
import tarfile
11+
import sys
12+
import os
13+
14+
15+
def main():
16+
argparser = argparse.ArgumentParser(description="Finds symbols from 'Required reason API' list")
17+
argparser.add_argument("-t", "--target", type=str, default="iosArm64", help='Kotlin target to inspect')
18+
argparser.add_argument("-v", "--verbose", help="enable verbose output", action="store_true")
19+
20+
args = argparser.parse_args()
21+
verbose = args.verbose
22+
target = args.target
23+
24+
konan_data_dir_env = os.getenv("KONAN_DATA_DIR")
25+
if konan_data_dir_env is not None:
26+
konan_data_dir = Path(konan_data_dir_env)
27+
else:
28+
konan_data_dir = Path.home() / ".konan"
29+
konan_data_dir.mkdir(exist_ok=True)
30+
31+
if platform.machine() == "arm64":
32+
arch = "aarch64"
33+
else:
34+
arch = "x86_64"
35+
36+
# TODO make it possible to use existing RC1/RC3/2.0.0 versions
37+
kotlin_version = "2.0.0-RC2"
38+
konan_home = konan_data_dir / f"kotlin-native-prebuilt-macos-{arch}-{kotlin_version}"
39+
40+
if not konan_home.exists():
41+
download_dist(konan_home, kotlin_version, arch)
42+
43+
libraries = filter(library_filter, retrieve_libraries_from_gradle_project(target, verbose))
44+
libraries = list(map(lambda it: it.strip(), libraries))
45+
46+
all_clear = True
47+
all_clear = check_for_compose(libraries) and all_clear
48+
49+
for library in libraries:
50+
all_clear = check_library(konan_home, library, verbose) and all_clear
51+
52+
if all_clear:
53+
print("\n" + green("No usages of required reason APIs found"))
54+
55+
56+
def download_dist(konan_home: Path, kotlin_version: str, arch: str):
57+
with tempfile.TemporaryDirectory() as tmp_dir:
58+
tmp_path = Path(tmp_dir)
59+
tar = f"kotlin-native-prebuilt-{kotlin_version}-macos-{arch}"
60+
tar_path = tmp_path.with_suffix(".tar.gz")
61+
url = f"https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-native-prebuilt/{kotlin_version}/{tar}.tar.gz"
62+
63+
print(f"Downloading {url}... ", end="")
64+
urllib.request.urlretrieve(url, tar_path)
65+
print(f"success!")
66+
67+
print(f"Extracting into {konan_home}... ", end="")
68+
with tarfile.open(tar_path, "r:gz") as tar_file:
69+
tar_file.extractall(path=tmp_path)
70+
os.rename(tmp_path / os.path.basename(konan_home), konan_home)
71+
print(f"success!")
72+
73+
74+
def find_gradlew(directory: Path):
75+
gradlew_file = directory / 'gradlew'
76+
if gradlew_file.exists():
77+
return gradlew_file
78+
else:
79+
parent = directory.parent
80+
if parent == directory:
81+
raise FileNotFoundError(f"Can't find gradlew in parent directories of {Path.cwd()}")
82+
return find_gradlew(parent)
83+
84+
85+
def capitalize_first_letter(s: str):
86+
return s[0].upper() + s[1:]
87+
88+
89+
def run_gradle_with_injected_code(injected_kts: str, injected: str, error: Exception, args: list[str], verbose: bool):
90+
working_dir = Path.cwd()
91+
gradlew_path = find_gradlew(working_dir)
92+
93+
build_gradle_kts = working_dir / "build.gradle.kts"
94+
build_gradle = working_dir / "build.gradle"
95+
96+
if build_gradle_kts.exists():
97+
build_gradle = build_gradle_kts
98+
injected = injected_kts
99+
elif not build_gradle.exists():
100+
raise FileNotFoundError(f"build.gradle.kts or build.gradle not found in {working_dir}")
101+
102+
build_gradle_backup = build_gradle.with_suffix(".backup")
103+
copyfile(build_gradle, build_gradle_backup)
104+
try:
105+
with open(build_gradle, 'a+') as file:
106+
file.write(injected)
107+
result = subprocess.run(
108+
args=[str(gradlew_path)] + args,
109+
cwd=working_dir,
110+
stdout=None if verbose else DEVNULL,
111+
stderr=None if verbose else PIPE,
112+
)
113+
if result.returncode != 0:
114+
if result.stderr is not None:
115+
sys.stderr.buffer.write(result.stderr)
116+
sys.stderr.flush()
117+
raise error
118+
finally:
119+
os.replace(build_gradle_backup, build_gradle)
120+
121+
122+
def retrieve_libraries_from_gradle_project(target: str, verbose: bool) -> list[str]:
123+
target = capitalize_first_letter(target)
124+
125+
build_gradle_kts_inject = """
126+
run {
127+
val compileTask = tasks.named<org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile>("compileKotlin$target")
128+
tasks.register(compileTask.name + "Libraries") {
129+
val libraries = compileTask.map { it.libraries + it.outputFile.get() }
130+
val output = file("$name.txt")
131+
val mimallocWarning = org.jetbrains.kotlin.tooling.core.KotlinToolingVersion(org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion(logger)) < org.jetbrains.kotlin.tooling.core.KotlinToolingVersion("1.9.20")
132+
dependsOn(compileTask)
133+
doFirst {
134+
if (mimallocWarning) output.appendText("{{kotlin_mimalloc_warning}}\\n")
135+
libraries.get().forEach { output.appendText(it.absolutePath + "\\n") }
136+
}
137+
}
138+
}
139+
""".replace("$target", target)
140+
141+
build_gradle_inject = """
142+
def _compileTask = tasks.named("compileKotlin$target")
143+
tasks.register(_compileTask.name + "Libraries") {
144+
def libraries = _compileTask.map { it.libraries + it.outputFile.get() }
145+
def output = file("${name}.txt")
146+
def mimallocWarning = org.jetbrains.kotlin.tooling.core.KotlinToolingVersionKt.KotlinToolingVersion(org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapperKt.getKotlinPluginVersion(logger)) < org.jetbrains.kotlin.tooling.core.KotlinToolingVersionKt.KotlinToolingVersion("1.9.20")
147+
dependsOn(_compileTask)
148+
doFirst {
149+
if (mimallocWarning) output.append("{{kotlin_mimalloc_warning}}\\n")
150+
libraries.get().forEach { output.append(it.absolutePath + "\\n") }
151+
}
152+
}
153+
""".replace("$target", target)
154+
155+
libraries_task = f"compileKotlin{target}Libraries"
156+
libraries_txt = Path.cwd() / (libraries_task + ".txt")
157+
158+
error = ChildProcessError(f"Gradle invocation failed. Please check that the project can be compiled with '$PROJECT_ROOT/gradlew compileKotlin{target}'")
159+
160+
try:
161+
print("Running Gradle to retrieve the list of dependencies... ", end="\n" if verbose else "")
162+
run_gradle_with_injected_code(build_gradle_kts_inject, build_gradle_inject, error, [libraries_task], verbose)
163+
if not verbose:
164+
print("success!")
165+
166+
with open(libraries_txt, 'r') as file:
167+
return file.readlines()
168+
finally:
169+
if libraries_txt.exists():
170+
libraries_txt.unlink()
171+
172+
173+
def library_filter(library: str):
174+
special = library.startswith("{{")
175+
name = os.path.basename(library).strip()
176+
platform_lib = name.startswith("org.jetbrains.kotlin.native.platform.")
177+
cinterop = ("-cinterop-" in name)
178+
klib = name.endswith(".klib")
179+
return special or (klib and not platform_lib and not cinterop)
180+
181+
182+
def dump_imported_platform_signatures(konan_home: Path, library: str, verbose: bool):
183+
process = subprocess.run(
184+
args=[
185+
str(konan_home / "bin/klib"),
186+
"dump-ir-signatures",
187+
library,
188+
],
189+
stdout=PIPE,
190+
stderr=None if verbose else DEVNULL,
191+
)
192+
193+
if process.returncode != 0:
194+
print(f"warning: Failed to dump signatures from {library}, skipping")
195+
return list()
196+
197+
lines = process.stdout.decode().splitlines()
198+
return list(filter(lambda line: line.startswith("platform."), lines))
199+
200+
201+
def check_library(konan_home: Path, library: str, verbose: bool) -> bool:
202+
if library == "{{kotlin_mimalloc_warning}}":
203+
return print_kotlin_mimalloc_warning()
204+
205+
required_reason_symbols = [
206+
# File timestamp APIs
207+
"platform.Foundation/NSFileCreationDate",
208+
"platform.Foundation/NSFileModificationDate",
209+
"platform.UIKit/UIDocument.fileModificationDate",
210+
"platform.Foundation/fileModificationDate",
211+
"platform.Foundation/NSURLContentModificationDateKey",
212+
"platform.Foundation/NSURLCreationDateKey",
213+
"platform.posix/getattrlist",
214+
"platform.posix/getattrlistbulk",
215+
"platform.posix/fgetattrlist",
216+
"platform.posix/stat",
217+
"platform.posix/fstat",
218+
"platform.posix/fstatat",
219+
"platform.posix/lstat",
220+
"platform.posix/getattrlistat",
221+
222+
# System boot time APIs
223+
"platform.Foundation/NSProcessInfo.systemUptime",
224+
"platform.darwin/mach_absolute_time",
225+
226+
# Disk space APIs
227+
"platform.Foundation/NSURLVolumeAvailableCapacityKey",
228+
"platform.Foundation/NSURLVolumeAvailableCapacityForImportantUsageKey",
229+
"platform.Foundation/NSURLVolumeAvailableCapacityForOpportunisticUsageKey",
230+
"platform.Foundation/NSURLVolumeTotalCapacityKey",
231+
"platform.Foundation/NSFileSystemFreeSize",
232+
"platform.Foundation/NSFileSystemSize",
233+
"platform.posix/getattrlist",
234+
"platform.posix/fgetattrlist",
235+
"platform.posix/getattrlistat",
236+
237+
# Active keyboard APIs
238+
"platform.UIKit/UITextInputMode.Companion.activeInputModes",
239+
240+
# User defaults APIs
241+
"platform.Foundation/NSUserDefaults",
242+
]
243+
244+
used_symbols = []
245+
246+
library_name = os.path.basename(library)
247+
signatures = dump_imported_platform_signatures(konan_home, library, verbose)
248+
for symbol in required_reason_symbols:
249+
for signature in signatures:
250+
if signature.startswith(symbol):
251+
used_symbols.append(symbol)
252+
break
253+
254+
if used_symbols:
255+
print(f"\nFound usages of required reason API in {yellow(library_name)} from {library}")
256+
print("\t" + "\n\t".join(used_symbols))
257+
return False
258+
elif verbose:
259+
print(f"\nNo usages of required reason API in {green(library_name)} from {library}")
260+
261+
return True
262+
263+
264+
def print_kotlin_mimalloc_warning():
265+
print(f"""
266+
Kotlin is using {yellow("mach_absolute_time")} from the required reason API list in versions lower than 1.9.20
267+
\tSee more details here: https://kotl.in/kkrs8t""")
268+
return False
269+
270+
271+
def check_for_compose(libraries: List[str]) -> bool:
272+
for library in libraries:
273+
if os.path.basename(library) == "skiko.klib":
274+
print(f"""
275+
Compose Multiplatform for iOS is using {yellow("stat")} and {yellow("fstat")} from from the required reason API list
276+
\tSee more details here: https://kotl.in/rx45vh""")
277+
return False
278+
return True
279+
280+
281+
def yellow(value: str) -> str:
282+
return Colors.yellow + value + Colors.reset
283+
284+
285+
def green(value: str) -> str:
286+
return Colors.green + value + Colors.reset
287+
288+
289+
class Colors:
290+
reset = "\u001B[0m"
291+
green = "\u001B[32m"
292+
yellow = "\u001B[33m"
293+
294+
295+
main()

0 commit comments

Comments
 (0)