KMM Signing Pod Dependency for Xcode 14 - xcode

I've recently updated to Xcode 14 for my KMM project and I now run into this build error when syncing gradle.
shared/build/cocoapods/synthetic/IOS/Pods/Pods.xcodeproj: error: Signing for "gRPC-C++-gRPCCertificates-Cpp" requires a development team. Select a development team in the Signing & Capabilities editor. (in target 'gRPC-C++-gRPCCertificates-Cpp' from project 'Pods')
I'm using Firebase and am including the pods as dependencies in my shared build.gradle file.
kotlin {
...
cocoapods {
...
ios.deploymentTarget = "15.0"
podfile = project.file("../iosApp/Podfile")
framework {
baseName = "shared"
}
xcodeConfigurationToNativeBuildType
val firebaseVersion = "9.6.0"
pod("FirebaseCore") { version = firebaseVersion }
pod("FirebaseAuth") { version = firebaseVersion }
pod("FirebaseFirestore") { version = firebaseVersion }
pod("FirebaseCrashlytics") { version = firebaseVersion }
pod("FirebaseAnalytics") { version = firebaseVersion }
}
...
}
I have a team setup and if I open Xcode and go to the Pods project, select gRPC-C++-gRPCCertificates-Cpp, then add my team to it, the project builds fine from Xcode.
However, this doesn't fix the gradle issue as it seems to use its own temporary project file when syncing (in shared/build/cocoapods/synthetic/IOS/Pods/Pods.xcodepro)
Is there any way to add my team to the gradle sync build?
As a temporary work around, I've switched back to Xcode 13.4.1 which doesn't have this problem.

This will be fixed in Kotlin 1.8.0 but in the meantime here is a workaround (from https://youtrack.jetbrains.com/issue/KT-54161)
First create the directories buildSrc/src/main/kotlin in the root of your project. In buildSrc create the file build.gradle.kts and add the following contents.
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
google()
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20")
implementation("com.android.tools.build:gradle:7.3.1")
}
Then create the file PatchedPodGenTask.kt in buildSrc/src/main/kotlin. Add the following to it.
import org.gradle.api.Project
import org.gradle.api.logging.Logger
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.*
import java.io.File
import java.io.IOException
import org.jetbrains.kotlin.konan.target.*
import org.jetbrains.kotlin.gradle.targets.native.tasks.PodGenTask
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.SpecRepos
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency.PodLocation.*
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.CocoapodsDependency.PodLocation
import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType
import kotlin.concurrent.thread
import kotlin.reflect.KClass
//import org.jetbrains.kotlin.gradle.plugin.cocoapods.cocoapodsBuildDirs
private val Family.platformLiteral: String
get() = when (this) {
Family.OSX -> "macos"
Family.IOS -> "ios"
Family.TVOS -> "tvos"
Family.WATCHOS -> "watchos"
else -> throw IllegalArgumentException("Bad family ${this.name}")
}
internal val Project.cocoapodsBuildDirs: CocoapodsBuildDirs
get() = CocoapodsBuildDirs(this)
internal class CocoapodsBuildDirs(val project: Project) {
val root: File
get() = project.buildDir.resolve("cocoapods")
val framework: File
get() = root.resolve("framework")
val defs: File
get() = root.resolve("defs")
val buildSettings: File
get() = root.resolve("buildSettings")
val synthetic: File
get() = root.resolve("synthetic")
fun synthetic(family: Family) = synthetic.resolve(family.name)
val externalSources: File
get() = root.resolve("externalSources")
val publish: File = root.resolve("publish")
fun externalSources(fileName: String) = externalSources.resolve(fileName)
fun fatFramework(buildType: NativeBuildType) =
root.resolve("fat-frameworks/${buildType.getName()}")
}
/**
* The task generates a synthetic project with all cocoapods dependencies
*/
open class PatchedPodGenTask : PodGenTask() {
private val PODFILE_SUFFIX = """
post_install do |installer|
installer.pods_project.build_configurations.each do |config|
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
end
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
end
end
installer.generated_projects.each do |project|
project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings["DEVELOPMENT_TEAM"] = "..."
end
end
end
end
""".trimIndent()
#TaskAction
fun patchedGenerate() {
val syntheticDir = project.cocoapodsBuildDirs.synthetic(family).apply { mkdirs() }
val specRepos: Collection<String> = specReposAccessor.get().getAllAccessor()
val projResource = "/cocoapods/project.pbxproj"
val projDestination = syntheticDir.resolve("synthetic.xcodeproj").resolve("project.pbxproj")
projDestination.parentFile.mkdirs()
projDestination.outputStream().use { file ->
javaClass.getResourceAsStream(projResource).use { resource ->
resource.copyTo(file)
}
}
val podfile = syntheticDir.resolve("Podfile")
podfile.createNewFile()
val podfileContent = getPodfileContent(specRepos, family.platformLiteral) + PODFILE_SUFFIX
podfile.writeText(podfileContent)
val podInstallCommand = listOf("pod", "install")
runCommand(
podInstallCommand,
project.logger,
exceptionHandler = { e: IOException ->
CocoapodsErrorHandlingUtil.handle(e, podInstallCommand)
},
errorHandler = { retCode, output, _ ->
CocoapodsErrorHandlingUtil.handlePodInstallSyntheticError(
podInstallCommand.joinToString(" "),
retCode,
output,
family,
podNameAccessor.get()
)
},
processConfiguration = {
directory(syntheticDir)
})
val podsXcprojFile = podsXcodeProjDirAccessor.get()
check(podsXcprojFile.exists() && podsXcprojFile.isDirectory) {
"Synthetic project '${podsXcprojFile.path}' was not created."
}
}
private fun getPodfileContent(specRepos: Collection<String>, xcodeTarget: String) =
buildString {
specRepos.forEach {
appendLine("source '$it'")
}
appendLine("target '$xcodeTarget' do")
//if (useLibraries.get().not()) {
appendLine("\tuse_frameworks!")
//}
pods.get().mapNotNull {
buildString {
append("pod '${it.name}'")
val pathType = when (it.source) {
is Path -> "path"
is Url -> "path"
is Git -> "git"
else -> null
}
val path = it.source?.getLocalPathAccessor(project, it.name)
if (path != null && pathType != null) {
append(", :$pathType => '$path'")
}
}
}.forEach { appendLine("\t$it") }
appendLine("end\n")
}
}
fun <T : Any, R> KClass<T>.invokeDynamic(methodStart: String, instance: Any?, vararg params: Any?): R {
val method = this.java.methods.firstOrNull { it.name.startsWith(methodStart) } ?: error("Can't find accessor for $methodStart")
return method.invoke(instance, *params) as R
}
val PodGenTask.podsXcodeProjDirAccessor: Provider<File> get() = this::class.invokeDynamic("getPodsXcodeProjDir", this)
val PodGenTask.specReposAccessor: Provider<SpecRepos> get() = this::class.invokeDynamic("getSpecRepos", this)
val PodGenTask.podNameAccessor: Provider<String> get() = this::class.invokeDynamic("getPodName", this)
fun SpecRepos.getAllAccessor(): Collection<String> = this::class.invokeDynamic("getAll", this)
fun PodLocation.getLocalPathAccessor(project: Project, podName: String): String? = this::class.invokeDynamic("getLocalPath", this, project, podName)
private fun runCommand(
command: List<String>,
logger: Logger,
errorHandler: ((retCode: Int, output: String, process: Process) -> String?)? = null,
exceptionHandler: ((ex: IOException) -> Unit)? = null,
processConfiguration: ProcessBuilder.() -> Unit = { }
): String {
var process: Process? = null
try {
process = ProcessBuilder(command)
.apply {
this.processConfiguration()
}.start()
} catch (e: IOException) {
if (exceptionHandler != null) exceptionHandler(e) else throw e
}
if (process == null) {
throw IllegalStateException("Failed to run command ${command.joinToString(" ")}")
}
var inputText = ""
var errorText = ""
val inputThread = thread {
inputText = process.inputStream.use {
it.reader().readText()
}
}
val errorThread = thread {
errorText = process.errorStream.use {
it.reader().readText()
}
}
inputThread.join()
errorThread.join()
val retCode = process.waitFor()
logger.info(
"""
|Information about "${command.joinToString(" ")}" call:
|
|${inputText}
""".trimMargin()
)
check(retCode == 0) {
errorHandler?.invoke(retCode, inputText.ifBlank { errorText }, process)
?: """
|Executing of '${command.joinToString(" ")}' failed with code $retCode and message:
|
|$inputText
|
|$errorText
|
""".trimMargin()
}
return inputText
}
private object CocoapodsErrorHandlingUtil {
fun handle(e: IOException, command: List<String>) {
if (e.message?.contains("No such file or directory") == true) {
val message = """
|'${command.take(2).joinToString(" ")}' command failed with an exception:
| ${e.message}
|
| Full command: ${command.joinToString(" ")}
|
| Possible reason: CocoaPods is not installed
| Please check that CocoaPods v1.10 or above is installed.
|
| To check CocoaPods version type 'pod --version' in the terminal
|
| To install CocoaPods execute 'sudo gem install cocoapods'
|
""".trimMargin()
throw IllegalStateException(message)
} else {
throw e
}
}
fun handlePodInstallSyntheticError(command: String, retCode: Int, error: String, family: Family, podName: String): String? {
var message = """
|'pod install' command on the synthetic project failed with return code: $retCode
|
| Full command: $command
|
| Error: ${error.lines().filter { it.contains("[!]") }.joinToString("\n")}
|
""".trimMargin()
if (
error.contains("deployment target") ||
error.contains("no platform was specified") ||
error.contains(Regex("The platform of the target .+ is not compatible with `$podName"))
) {
message += """
|
| Possible reason: ${family.name.toLowerCase()} deployment target is not configured
| Configure deployment_target for ALL targets as follows:
| cocoapods {
| ...
| ${family.name.toLowerCase()}.deploymentTarget = "..."
| ...
| }
|
""".trimMargin()
return message
} else if (
error.contains("Unable to add a source with url") ||
error.contains("Couldn't determine repo name for URL") ||
error.contains("Unable to find a specification")
) {
message += """
|
| Possible reason: spec repos are not configured correctly.
| Ensure that spec repos are correctly configured for all private pod dependencies:
| cocoapods {
| specRepos {
| url("<private spec repo url>")
| }
| }
|
""".trimMargin()
return message
}
return null
}
}
Find the line that says config.build_settings["DEVELOPMENT_TEAM"] = "..." and replace ... with your actual dev team ID.
Finally, in the build.gradle.kts file of the module you want to apply it (mine was the shared module) add the following
// #TODO: This is a hack, and we should remove it once PodGenTask is fixed or supports adding suffixes in the official Kotlin plugin
tasks.replace("podGenIOS", PatchedPodGenTask::class.java)
Clean your project and re-sync Gradle. The patch should be applied and work with Xcode 14

Related

Kotlin : copy image to gallery

I am trying to copy an image that is stored in my application folder to a predefined folder in my gallery.
I started from an image sharing code..
This is my code :
val extension = when (requireNotNull(pictureResult).format) {
PictureFormat.JPEG -> "jpg"
PictureFormat.DNG -> "dng"
else -> throw RuntimeException("Unknown format.")
}
val timestamp = System.currentTimeMillis()
val namePhoto = "picture_"+timestamp+"."+extension;
val destFile = File(filesDir, namePhoto)
val folder = "/CustomFolder"
CameraUtils.writeToFile(requireNotNull(pictureResult?.data), destFile) { file ->
if (file != null) {
// Code to share - it works
/*
val context = this#PicturePreviewActivity
val intent = Intent(Intent.ACTION_SEND)
intent.type = "image/*"
val uri = FileProvider.getUriForFile(context, context.packageName + ".provider", file)
intent.putExtra(Intent.EXTRA_STREAM, uri)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivity(intent)
*/
*/
// Code to save image to gallery - doesn't work :(
val photoDirectory = File(Environment.DIRECTORY_PICTURES+folder, namePhoto)
val sourcePath = Paths.get(file.toURI())
Log.i("LOG","sourcePath : "+sourcePath.toString()) // /data/user/0/com.app.demo/files/picture_1663772068143.jpg
val targetPath = Paths.get(photoDirectory.toURI())
Log.i("LOG","targetPath : "+targetPath.toString()) // /Pictures/CustomFolder/picture_1663772068143.jpg
Files.move(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING)
// Error here but I don't know why
} else {
Toast.makeText(this#PicturePreviewActivity, "Error while writing file.", Toast.LENGTH_SHORT).show()
}
}
How do I copy the image to a predefined folder?
Ok, I did it !
Solution :
val folder = "/CustomFolder/" // name of your folder
val timestamp = System.currentTimeMillis()
val namePicture = "picture_"+timestamp+"."+extension;
try {
val path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES+folder);
if (!path.exists()) {
path.mkdir();
}
val pathImage = File(path,namePicture)
val stream = FileOutputStream(pathImage)
stream.write(imageByteArray).run {
stream.flush()
stream.close()
}
} catch (e: FileNotFoundException) {
e.printStackTrace()
}

Kotlin-native execute command and get the output

I'd like to know if there's a way in kotlin native to call a command via posix and receive it's terminal output. For example, I'd like to get the "git diff" command working without having to create a temporary file, write output to it and then read from that file.
On SO I've only found solutions requiring ProcessBuilder, which isn't available on kotlin-native, as it's a Java library.
I found a working piece of code I wanted to use, so I'm posting it here for future viewers!
fun executeCommand(command: String): String{
val fp: CPointer<FILE>? = popen(command, "r")
val buffer = ByteArray(4096)
val returnString = StringBuilder()
/* Open the command for reading. */
if (fp == NULL) {
printf("Failed to run command\n" )
exit(1)
}
/* Read the output a line at a time - output it. */
var scan = fgets(buffer.refTo(0), buffer.size, fp)
if(scan != null) {
while (scan != NULL) {
returnString.append(scan!!.toKString())
scan = fgets(buffer.refTo(0), buffer.size, fp)
}
}
/* close */
pclose(fp)
return returnString.trim().toString()
}
It's an improved version of exec command for Kotlin Native posted by mg-lolenstine, it throws an exception with command stderr instead of just returning exit(1) (which not always desirable behavior itself), also trim is now optional
import kotlinx.cinterop.*
import platform.posix.*
fun executeCommand(
command: String,
trim: Boolean = true,
redirectStderr: Boolean = true
): String {
val commandToExecute = if (redirectStderr) "$command 2>&1" else command
val fp = popen(commandToExecute, "r") ?: error("Failed to run command: $command")
val stdout = buildString {
val buffer = ByteArray(4096)
while (true) {
val input = fgets(buffer.refTo(0), buffer.size, fp) ?: break
append(input.toKString())
}
}
val status = pclose(fp)
if (status != 0) {
error("Command `$command` failed with status $status${if (redirectStderr) ": $stdout" else ""}")
}
return if (trim) stdout.trim() else stdout
}
Usually you can use the POSIX api and use fork and wait and some I/O related functions for your purpose
fun main() {
val childPid: pid_t = fork()
if (childPid == 0) {
val commands = listOf("git", "diff", "HEAD^1", "$projectDir/path/to/file", null)
val cwd = "$projectDir"
chdir(cwd)
memScoped {
execvp(commands[0], allocArrayOf(commands.map { it?.cstr?.ptr }))
}
} else {
wait(null)
}
}
Of course, this needs to deal with a lot of c-style code, so I also wrote a more practical library for this
repositories {
mavenCentral()
}
// add dependencies into your native target sourceSet
dependencies {
implementation("com.kgit2:kommand:0.1.4")
}
It is also very simple to use
fun main(args: Array<String>) {
val diffResult = Command("git")
.args("diff", "HEAD^1", "$projectDir/path/to/file")
.cwd("$projectDir")
.spawn()
.output()
}
playing around I used above's answers to create a working gradle kotlin native/jvm multiplatform multiproject that runs arbitrary local Processes/Commands:
here's my result:
https://github.com/hoffipublic/minimal_kotlin_multiplatform
import kotlinx.cinterop.refTo
import kotlinx.cinterop.toKString
import platform.posix.fgets
import platform.posix.pclose
import platform.posix.popen
actual object MppProcess : IMppProcess {
actual override fun executeCommand(
command: String,
redirectStderr: Boolean
): String? {
val commandToExecute = if (redirectStderr) "$command 2>&1" else command
val fp = popen(commandToExecute, "r") ?: error("Failed to run command: $command")
val stdout = buildString {
val buffer = ByteArray(4096)
while (true) {
val input = fgets(buffer.refTo(0), buffer.size, fp) ?: break
append(input.toKString())
}
}
val status = pclose(fp)
if (status != 0) {
error("Command `$command` failed with status $status${if (redirectStderr) ": $stdout" else ""}")
}
return stdout
}
}
on jvm
import java.util.concurrent.TimeUnit
actual object MppProcess : IMppProcess {
actual override fun executeCommand(
command: String,
redirectStderr: Boolean
): String? {
return runCatching {
ProcessBuilder(command.split(Regex("(?<!(\"|').{0,255}) | (?!.*\\1.*)")))
//.directory(workingDir)
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.apply { if (redirectStderr) this.redirectError(ProcessBuilder.Redirect.PIPE) }
.start().apply { waitFor(60L, TimeUnit.SECONDS) }
.inputStream.bufferedReader().readText()
}.onFailure { it.printStackTrace() }.getOrNull()
}
}

Unzip a file in kotlin script [.kts]

I was toying with the idea of rewriting some existing bash scripts in kotlin script.
One of the scripts has a section that unzips all the files in a directory. In bash:
unzip *.zip
Is there a nice way to unzip a file(s) in kotlin script?
The easiest way is to just use exec unzip (assuming that the name of your zip file is stored in zipFileName variable):
ProcessBuilder()
.command("unzip", zipFileName)
.redirectError(ProcessBuilder.Redirect.INHERIT)
.redirectOutput(ProcessBuilder.Redirect.INHERIT)
.start()
.waitFor()
The different approach, that is more portable (it will run on any OS and does not require unzip executable to be present), but somewhat less feature-full (it will not restore Unix permissions), is to do unzipping in code:
import java.io.File
import java.util.zip.ZipFile
ZipFile(zipFileName).use { zip ->
zip.entries().asSequence().forEach { entry ->
zip.getInputStream(entry).use { input ->
File(entry.name).outputStream().use { output ->
input.copyTo(output)
}
}
}
}
If you need to scan all *.zip file, then you can do it like this:
File(".").list { _, name -> name.endsWith(".zip") }?.forEach { zipFileName ->
// any of the above approaches
}
or like this:
import java.nio.file.*
Files.newDirectoryStream(Paths.get("."), "*.zip").forEach { path ->
val zipFileName = path.toString()
// any of the above approaches
}
this code is for unziping from Assets
1.for unzping first u need InputStream
2.put it in ZipInputStream
3.if directory is not exist u have to make by .mkdirs()
private val BUFFER_SIZE = 8192//2048;
private val SDPath = Environment.getExternalStorageDirectory().absolutePath
private val unzipPath = "$SDPath/temp/zipunzipFile/unzip/"
var count: Int
val buffer = ByteArray(BUFFER_SIZE)
val context: Context = this
val am = context.getAssets()
val stream = context.getAssets().open("raw.zip")
try {
ZipInputStream(stream).use { zis ->
var ze: ZipEntry
while (zis.nextEntry.also { ze = it } != null) {
var fileName = ze.name
fileName = fileName.substring(fileName.indexOf("/") + 1)
val file = File(unzipPath, fileName)
val dir = if (ze.isDirectory) file else file.getParentFile()
if (!dir.isDirectory() && !dir.mkdirs())
throw FileNotFoundException("Invalid path: " + dir.getAbsolutePath())
if (ze.isDirectory) continue
val fout = FileOutputStream(file)
try {
while ( zis.read(buffer).also { count = it } != -1)
fout.write(buffer, 0, count)
} finally {
val fout : FileOutputStream =openFileOutput(fileName, Context.MODE_PRIVATE)
fout.close()
}
}
for unziping from externalStorage:
private val sourceFile= "$SDPath/unzipFile/data/"
ZipInputStream zis = null;
try {
zis = new ZipInputStream(new BufferedInputStream(new
FileInputStream(sourceFile)));
ZipEntry ze;
int count;
byte[] buffer = new byte[BUFFER_SIZE];
while ((ze = zis.getNextEntry()) != null) {
String fileName = ze.getName();
fileName = fileName.substring(fileName.indexOf("/") + 1);
File file = new File(destinationFolder, fileName);
File dir = ze.isDirectory() ? file : file.getParentFile();
if (!dir.isDirectory() && !dir.mkdirs())
throw new FileNotFoundException("Invalid path: " +
dir.getAbsolutePath());
if (ze.isDirectory()) continue;
FileOutputStream fout = new FileOutputStream(file);
try {
while ((count = zis.read(buffer)) != -1)
fout.write(buffer, 0, count);
} finally {
fout.close();
}
}
} catch (IOException ioe) {
Log.d(TAG, ioe.getMessage());
return false;
} finally {
if (zis != null)
try {
zis.close();
} catch (IOException e) {
}
}
return true;

annotation macro that rewrites and impls a trait, generics not processed correctly

I am writing a macro that needs to create a class that rewrites a trait, having the same methods/args of the trait but different return type.
So say we got:
trait MyTrait[T]
{
def x(t1: T)(t2: T): T
}
#AnnProxy
class MyClass[T] extends MyTrait[T]
MyClass will be rewritten to:
class MyClass[T] {
def x(t1: T)(t2: T): R[T]
}
(so x will now return R[T] instead of T)
I wrote the macro and debugging it, it produces this code:
Expr[Any](class MyClass[T] extends scala.AnyRef {
def <init>() = {
super.<init>();
()
};
def x(t1: T)(t2: T): macrotests.R[T] = $qmark$qmark$qmark
})
#AnnProxy
As you see the signature seems ok. But when trying to use the macro, I get a compilation error:
val my = new MyClass[Int]
my.x(5)(6)
Error:(14, 7) type mismatch;
found : Int(5)
required: T
x.x(5)(6)
^
So it seems the method's generic T is not the same as the class [T]. Any ideas how to fix?
This is my macro so far. I am not any good with macros (coin'd this up with a lot of help from stackoverflow), but this is the current state:
#compileTimeOnly("enable macro paradise to expand macro annotations")
class AnnProxy extends StaticAnnotation
{
def macroTransform(annottees: Any*): Any = macro IdentityMacro.impl
}
trait R[T]
object IdentityMacro
{
private val SDKClasses = Set("java.lang.Object", "scala.Any")
def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
def showInfo(s: String) = c.info(c.enclosingPosition, s.split("\n").mkString("\n |---macro info---\n |", "\n |", ""), true)
val classDef = annottees.map(_.tree).head.asInstanceOf[ClassDef]
val clazz = c.typecheck(classDef).symbol.asClass
val tparams = clazz.typeParams
val baseClasses = clazz.baseClasses.tail.filter(clz => !SDKClasses(clz.fullName))
val methods = baseClasses.flatMap {
base =>
base.info.decls.filter(d => d.isMethod && d.isPublic).map { decl =>
val termName = decl.name.toTermName
val method = decl.asMethod
val params = method.paramLists.map(_.map {
s =>
val vd = internal.valDef(s)
val f = tparams.find(_.name == vd.tpt.symbol.name)
val sym = if (f.nonEmpty) f.get else vd.tpt.symbol
q"val ${vd.name} : $sym "
})
val paramVars = method.paramLists.flatMap(_.map(_.name))
q""" def $termName (...$params)(timeout:scala.concurrent.duration.FiniteDuration) : macrotests.R[${method.returnType}] = {
???
}"""
}
}
val cde = c.Expr[Any] {
q"""
class ${classDef.name} [..${classDef.tparams}] {
..$methods
}
"""
}
showInfo(show(cde))
cde
}
}
EDIT: I managed to work around by building the class as a string and then using c.parse to compile it. Feels like a hack but it works. There must be a better way by manipulating the tree.
package macrotests
import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.language.experimental.macros
import scala.reflect.macros.whitebox
#compileTimeOnly("enable macro paradise to expand macro annotations")
class AnnProxy extends StaticAnnotation
{
def macroTransform(annottees: Any*): Any = macro AnnProxyMacro.impl
}
trait R[T]
trait Remote[T]
object AnnProxyMacro
{
private val SDKClasses = Set("java.lang.Object", "scala.Any")
def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
val classDef = annottees.map(_.tree).head.asInstanceOf[ClassDef]
val clazz = c.typecheck(classDef).symbol.asClass
val baseClasses = clazz.baseClasses.tail.filter(clz => !SDKClasses(clz.fullName))
val methods = baseClasses.flatMap {
base =>
base.info.decls.filter(d => d.isMethod && d.isPublic).map { decl =>
val termName = decl.name.toTermName
val method = decl.asMethod
val params = method.paramLists.map(_.map {
s =>
val vd = internal.valDef(s)
val tq = vd.tpt
s"${vd.name} : $tq"
})
val paramVars = method.paramLists.flatMap(_.map(_.name))
val paramVarsArray = paramVars.mkString("Array(", ",", ")")
val paramsStr = params.map(_.mkString("(", ",", ")")).mkString(" ")
val retTpe = method.returnType.typeArgs.mkString("-unexpected-")
s""" def $termName $paramsStr (timeout:scala.concurrent.duration.FiniteDuration) : macrotests.Remote[$retTpe] = {
println($paramVarsArray.toList)
new macrotests.Remote[$retTpe] {}
}"""
}
}
val tparams = clazz.typeParams.map(_.name)
val tparamsStr = if (tparams.isEmpty) "" else tparams.mkString("[", ",", "]")
val code =
s"""
|class ${classDef.name}$tparamsStr (x:Int) {
|${methods.mkString("\n")}
|}
""".stripMargin
// print(code)
val cde = c.Expr[Any](c.parse(code))
cde
}
}
the code is very long , you can look at the github: https://github.com/1178615156/scala-macro-example/blob/master/stackoverflow/src/main/scala/so/AnnotationWithTrait.scala
import scala.annotation.StaticAnnotation
import scala.language.experimental.macros
import scala.reflect.macros.blackbox.Context
/**
* Created by yu jie shui on 2015/12/2.
*/
class AnnotationWithTrait extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro AnnotationWithTraitImpl.apply
}
class AnnotationWithTraitImpl(val c: Context) {
import c.universe._
val SDKClasses = Set("java.lang.Object", "scala.Any")
def showInfo(s: String) = c.info(c.enclosingPosition, s.split("\n").mkString("\n |---macro info---\n |", "\n |", ""), true)
def apply(annottees: c.Expr[Any]*) = {
val classDef = annottees.map(_.tree).head.asInstanceOf[ClassDef]
val superClassSymbol= c.typecheck(classDef).symbol.asClass.baseClasses.tail
.filterNot(e => SDKClasses.contains(e.fullName)).reverse
val superClassTree= classDef match {
case q"$mod class $name[..$t](..$params) extends ..$superClass { ..$body }" =>
(superClass: List[Tree]).filterNot(e =>
typeOf[Object].members.exists(_.name == e.children.head.toString())
)
}
showInfo(show(superClassSymbol))
showInfo(show(superClassTree))
val impl = q"private[this] object ${TermName("impl")} extends ..${superClassTree}"
//
//get super class all can call method
val methods = superClassSymbol.map(_.info.members
.filterNot(_.isConstructor)
.filterNot(e => typeOf[Object].members.exists(_.name == e.name)).map(_.asMethod)).toList
case class ReplaceTypeParams(from: String, to: String)
type ClassReplace = List[ReplaceTypeParams]
//trait a[A]
//class b[B] extends a[B]
//need replace type params A to B
val classReplaceList: List[ClassReplace] = superClassTree zip superClassSymbol map {
case (superClassTree, superClassSymbol) =>
superClassSymbol.asClass.typeParams.map(_.name) zip superClassTree.children.tail map
(e => ReplaceTypeParams(e._1.toString, e._2.toString()))
}
val out = classReplaceList zip methods map {
case (classReplace, func) =>
func map { e => {
val funcName = e.name
val funcTypeParams = e.typeParams.map(_.name.toString).map(name => {
TypeDef(Modifiers(Flag.PARAM), TypeName(name), List(), TypeBoundsTree(EmptyTree, EmptyTree))
})
val funcParams = e.paramLists.map(_.map(e => q"${e.name.toTermName}:${
TypeName(
classReplace.find(_.from == e.info.toString).map(_.to).getOrElse(e.info.toString)
)} "))
val funcResultType = TypeName(
classReplace.find(_.from == e.returnType.toString).map(_.to).getOrElse(e.info.toString)
)
q"""
def ${funcName}[..${funcTypeParams}](...$funcParams):${funcResultType}=
impl.${funcName}[..${funcTypeParams}](...$funcParams)
"""
}
}
}
showInfo(show(out))
q"""
class ${classDef.name}[..${classDef.tparams}]{
$impl
..${out.flatten}
}
"""
}
}
test
trait MyTrait[MT1] {
def x(t1: MT1)(t2: MT1): MT1 = t1
}
trait MyTrait2[MT2] {
def t(t2: MT2): MT2 = t2
}
#AnnotationWithTrait
class MyClass[MCT1, MCT2] extends MyTrait[MCT1] with MyTrait2[MCT2]
object AnnotationWithTraitUsing extends App {
assert(new MyClass[Int, String].x(1)(2) == 1)
assert(new MyClass[Int, String].t("aaa") == "aaa")
}

Can I get gradle build to fail when there is version conflict between two dependencies when one of them doesn't have group specified

I am trying to figure out how to get gradle to fail a build on a version conflict between two dependencies when one of them doesn't have group specified.
For example I have a project that depends on jar from configured flat directory repository (for a vendor)
compile ':guava:r09'
and a transitive dependency that points to module retrieved from maven repository
compile 'com.google.guava:guava:13.0.1'
I understand that by design failOnVersionConflict resolution strategy will not raise a conflict for the above example. I cannot determine group name for jars in flat directory and hence would like to raise a conflict where developer can force one of the module accordingly. Thank you for taking the time to read by question.
Update:
Based on Ben's comment, I am attaching the code snippet used to raise custom conflict. But this is not helpful in my case since I cannot take advantage of force resolution strategy to resolve the conflict. I could exclude transitive dependencies or remove direct dependency altogether. Hopefully it is useful to someone else.
gradle.afterProject {
project.configurations.each { conf ->
def map = new HashMap<String, List<Dependency>>()
//println "\tConfiguration- ${project.name}:${conf.name}"
conf.allDependencies.each { dep ->
//println "\t\t${dep.group}:${dep.name}:${dep.version}"
ArrayList<Dependency> dependencies = null
if(map.containsKey(dep.name))
{
dependencies = map.get(dep.name)
}
else
{
dependencies = new ArrayList<>()
map.put(dep.name, dependencies)
dependencies.add(dep)
}
if(dep.group == null || dep.group.equals("unspecified"))
{
for(Dependency depInMap : dependencies) {
if(depInMap.version == null && dep.version == null)
continue;
if(depInMap.version != null && depInMap.version.equals(dep.version))
continue;
throw new GradleException("Customized Conflict: A conflict was found in " +
"${project.name}:${conf.name} between the following modules:" +
"\n- ${dep.group}:${dep.name}:${dep.version}" +
"\n- ${depInMap.group}:${depInMap.name}:${depInMap.version}")
}
}
dependencies.add(dep);
}
}
}
Here is solution I came up with. Given below is code snippet for some one else who may have use for it. The code isn't neat but is functional. Any suggestions are welcome.
private static boolean isGroupEmpty(DependencyResolveDetails details){
if (details.requested.group == null) {
return true;
} else if (details.requested.group == "unspecified"){
return true;
} else if(details.requested.group.isEmpty()) {
return true;
} else {
return false;
}
}
configurations.all {
def dependencyMap = [:]
def forcedModules = resolutionStrategy.forcedModules
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
def targetToUse = null
for (forcedModule in forcedModules) {
if(forcedModule.name == details.requested.name &&
(forcedModule.group == details.requested.group || isGroupEmpty(details))) {
targetToUse = "${forcedModule.group}:${forcedModule.name}:${forcedModule.version}"
}
}
if(targetToUse != null) {
println "Forcing: " + targetToUse
details.useTarget targetToUse
}
else {
if(dependencyMap.containsKey(details.requested.name)) {
DependencyResolveDetails prevDetails = dependencyMap.get(details.requested.name);
boolean groupMatches = false
if(isGroupEmpty(prevDetails) || isGroupEmpty(details) ||
prevDetails.requested.group == details.requested.group){
groupMatches = true
}
if(groupMatches)
{
boolean versionMatches = false
if(prevDetails.requested.version == details.requested.version) {
versionMatches = true
}
if(!versionMatches)
{
//If versions don't match throw an exception.
throw new GradleException("Custom Conflict: A conflict was found in " +
"${project.name} between the following modules:" +
"\n- ${prevDetails.requested.group}:${prevDetails.requested.name}:" +
"${prevDetails.requested.version}" +
"\n- ${details.requested.group}:${details.requested.name}:" +
"${details.requested.version}")
}
else {
//Use either one (in this case I force the one with empty group)
DependencyResolveDetails repl
DependencyResolveDetails pref
if(isGroupEmpty(prevDetails)) {
pref = prevDetails
repl = details
}
else {
pref = details
repl = prevDetails
dependencyMap.put(details.requested.name, details)
}
repl.useTarget "unspecified:${pref.requested.name}:${pref.requested.version}"
println "Replacing module " +
"\n - ${repl.requested.group}:${repl.requested.name}:${repl.requested.version}" +
"\n with " +
"\n - ${pref.requested.group}:${pref.requested.name}:${pref.requested.version} ."
}
}
}
else {
dependencyMap.put(details.requested.name, details)
}
}
}
}

Resources