Skip to content

Commit 62c9cd0

Browse files
committed
Support Android Gradle Plugin 9 and migration
1 parent 878afb4 commit 62c9cd0

1 file changed

Lines changed: 97 additions & 0 deletions

File tree

Sources/SkipDrive/GradleHarness.swift

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,10 @@ extension GradleHarness {
332332
let projectArg = arguments.dropFirst(projectFlagIndex + 1).first {
333333
project.appendPathComponent(projectArg, isDirectory: true)
334334
}
335+
336+
// for app projects, warn about build.gradle.kts settings that are incompatible with newer AGP versions
337+
checkAppBuildGradle(in: project)
338+
335339
project.appendPathComponent("settings.gradle.kts", isDirectory: false)
336340

337341
if FileManager.default.fileExists(atPath: project.path) {
@@ -346,6 +350,24 @@ extension GradleHarness {
346350
}
347351
}
348352

353+
/// For an app project — detected by the presence of an `app/build.gradle.kts` application module in the
354+
/// Gradle project folder — warn about any `build.gradle.kts` settings that are incompatible with newer
355+
/// Android Gradle Plugin (AGP) versions. `skip verify --fix` can remove these settings automatically.
356+
func checkAppBuildGradle(in projectFolder: URL) {
357+
let appBuildGradle = projectFolder.appendingPathComponent("app/build.gradle.kts", isDirectory: false)
358+
guard FileManager.default.fileExists(atPath: appBuildGradle.path),
359+
let buildContents = try? String(contentsOf: appBuildGradle, encoding: .utf8) else {
360+
return // not an app project (no app module build.gradle.kts)
361+
}
362+
363+
for issue in AppBuildGradleAGPIssue.issues(inAppBuildGradle: buildContents) {
364+
// report the 1-based line number of the offending active setting so the warning is clickable in the IDE
365+
let line = buildContents.gradleLines().firstIndex(where: { $0.activelyContains(issue.trigger) }).map({ $0 + 1 }) ?? 0
366+
let gradleIssue = GradleIssue(kind: .warning, message: "Android/app/build.gradle.kts: " + issue.message, location: SourceLocation(path: appBuildGradle.path, position: SourceLocation.Position(line: line, column: 0)))
367+
print(gradleIssue.xcodeMessageString)
368+
}
369+
}
370+
349371
public func gradleExec(in projectFolder: URL?, moduleName: String?, packageName: String?, arguments: [String]) async throws {
350372
preprocessGradleArguments(in: projectFolder, arguments: arguments)
351373

@@ -453,6 +475,81 @@ public struct GradleIssue {
453475
}
454476
}
455477

478+
/// A setting in an app's `build.gradle.kts` that is incompatible with newer Android Gradle Plugin (AGP)
479+
/// versions. The same definitions drive both the build-time warning (in `preprocessGradleArguments`) and the
480+
/// automatic fix performed by `skip verify --fix`, so the two never drift apart. Detection and fixes only
481+
/// consider *active* (non-commented) lines, so a commented-out reference in the project template is ignored.
482+
public struct AppBuildGradleAGPIssue {
483+
/// The substring whose presence on an active line of `build.gradle.kts` indicates the issue.
484+
public let trigger: String
485+
/// A description of what should be removed and why, including a support link.
486+
public let message: String
487+
/// Returns the given `build.gradle.kts` contents with this issue removed.
488+
let removingIssue: (String) -> String
489+
490+
/// All AGP-compatibility issues that are checked for an app's `build.gradle.kts`.
491+
public static let all: [AppBuildGradleAGPIssue] = [
492+
// AGP 9 no longer ships the default proguard-android.txt that earlier SkipProject defaults referenced
493+
// in the line: proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
494+
AppBuildGradleAGPIssue(trigger: #"getDefaultProguardFile("proguard-android.txt")"#,
495+
message: #"remove the getDefaultProguardFile("proguard-android.txt") section to be compliant with Android Gradle Plugin (AGP) version 9. Details: https://forums.skip.dev/categories/announcements"#,
496+
removingIssue: { contents in
497+
// remove the getDefaultProguardFile(…) argument from active proguardFiles lines, keeping the rest
498+
contents.gradleLines().map { line in
499+
guard line.activelyContains(#"getDefaultProguardFile("proguard-android.txt")"#) else { return line }
500+
var fixed = line
501+
for variant in [
502+
#"getDefaultProguardFile("proguard-android.txt"), "#,
503+
#"getDefaultProguardFile("proguard-android.txt"),"#,
504+
#", getDefaultProguardFile("proguard-android.txt")"#,
505+
#"getDefaultProguardFile("proguard-android.txt")"#,
506+
] {
507+
fixed = fixed.replacingOccurrences(of: variant, with: "")
508+
}
509+
return fixed
510+
}.joined(separator: "\n")
511+
}),
512+
// the kotlin.android Gradle plugin is no longer needed for building Skip apps
513+
AppBuildGradleAGPIssue(trigger: "alias(libs.plugins.kotlin.android)",
514+
message: "remove the line alias(libs.plugins.kotlin.android) as it is no longer needed for building apps. Details: https://forums.skip.dev/categories/announcements",
515+
removingIssue: { contents in
516+
// remove active (non-commented) lines that declare the kotlin.android plugin
517+
contents.gradleLines().filter { !$0.activelyContains("alias(libs.plugins.kotlin.android)") }.joined(separator: "\n")
518+
}),
519+
]
520+
521+
/// Whether an active (non-commented) line in the given `build.gradle.kts` contains this issue's trigger.
522+
public func isActive(in contents: String) -> Bool {
523+
contents.gradleLines().contains { $0.activelyContains(trigger) }
524+
}
525+
526+
/// Returns the issues that are actively present in the given `build.gradle.kts` contents.
527+
public static func issues(inAppBuildGradle contents: String) -> [AppBuildGradleAGPIssue] {
528+
all.filter { $0.isActive(in: contents) }
529+
}
530+
531+
/// Returns the given `build.gradle.kts` contents with every actively-present AGP issue removed.
532+
public static func removingIssues(fromAppBuildGradle contents: String) -> String {
533+
var result = contents
534+
for issue in all where issue.isActive(in: result) {
535+
result = issue.removingIssue(result)
536+
}
537+
return result
538+
}
539+
}
540+
541+
private extension String {
542+
/// The lines of these gradle file contents, preserving empty lines.
543+
func gradleLines() -> [String] {
544+
split(separator: "\n", omittingEmptySubsequences: false).map(String.init)
545+
}
546+
547+
/// Whether this line contains `substring` as active content, i.e. the line is not a `//` comment.
548+
func activelyContains(_ substring: String) -> Bool {
549+
contains(substring) && !trimmingCharacters(in: .whitespaces).hasPrefix("//")
550+
}
551+
}
552+
456553
public struct NoModuleFolder : LocalizedError {
457554
public var errorDescription: String?
458555
}

0 commit comments

Comments
 (0)