@@ -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+
456553public struct NoModuleFolder : LocalizedError {
457554 public var errorDescription : String ?
458555}
0 commit comments