mirror of https://github.com/weisJ/darklaf.git
weisj
4 years ago
committed by
Jannis Weis
15 changed files with 421 additions and 711 deletions
@ -1,14 +0,0 @@
|
||||
import java.util.concurrent.Callable |
||||
|
||||
class CallableLogger extends OneTimeLogger implements Callable<List<File>> { |
||||
|
||||
CallableLogger(Runnable logger) { |
||||
super(logger) |
||||
} |
||||
|
||||
@Override |
||||
List<File> call() throws Exception { |
||||
super.log() |
||||
return Collections.emptyList() |
||||
} |
||||
} |
@ -1,360 +0,0 @@
|
||||
import groovy.json.JsonOutput |
||||
import groovy.json.JsonSlurper |
||||
import groovy.transform.CompileStatic |
||||
import org.gradle.api.DefaultTask |
||||
import org.gradle.api.GradleException |
||||
import org.gradle.api.Transformer |
||||
import org.gradle.api.tasks.OutputFile |
||||
|
||||
import java.nio.file.Files |
||||
import java.nio.file.Path |
||||
import java.nio.file.StandardCopyOption |
||||
import java.util.concurrent.locks.ReadWriteLock |
||||
import java.util.concurrent.locks.ReentrantReadWriteLock |
||||
import java.util.stream.Stream |
||||
import java.util.zip.ZipEntry |
||||
import java.util.zip.ZipFile |
||||
|
||||
@CompileStatic |
||||
class DownloadPrebuiltBinaryFromGitHubAction extends DefaultTask { |
||||
|
||||
private static final ReadWriteLock LOCK = new ReentrantReadWriteLock() |
||||
|
||||
private static final String VERSION_INFO_FILE_NAME = "github_artifact_versions.json" |
||||
private static final String TEMP_PATH = "tmp${File.separator}prebuild" |
||||
private static final String PRE_BUILD_PATH = "libs${File.separator}prebuild" |
||||
|
||||
private final OneTimeLogger tokenWarning = new OneTimeLogger.Static({ |
||||
error("""No github access token is specified. Latest artifacts will need to be included manually. |
||||
|The access token needs to have the 'read-public' property. Specify using: |
||||
| -PgithubAccessToken=<your token> |
||||
|or by setting |
||||
| githubAccessToken=<your token> |
||||
|inside the gradle.properties file. |
||||
|""".stripMargin()) |
||||
}, "missingTokenWarning") |
||||
private final OneTimeLogger useCachedWarning = new OneTimeLogger({ |
||||
log("Could not download artifact or artifact information. Using cached version") |
||||
}) |
||||
|
||||
private Map cacheInfo |
||||
|
||||
private String manualDownloadUrl = "" |
||||
private String user |
||||
private String repository |
||||
private String workflow |
||||
private List<String> branches = [] |
||||
private boolean missingLibraryIsFailure |
||||
private int timeout |
||||
|
||||
private String githubAccessToken |
||||
private String variant |
||||
private Optional<File> prebuiltBinary |
||||
|
||||
@OutputFile |
||||
File getPrebuiltBinaryFile() { |
||||
if (user == null) throw new GradleException("Github user isn't specified") |
||||
if (repository == null) repository = project.name |
||||
if (workflow == null) throw new GradleException("Workflow isn't specified") |
||||
|
||||
if (prebuiltBinary == null) { |
||||
if (githubAccessToken == null || githubAccessToken.isEmpty()) { |
||||
tokenWarning.log() |
||||
} |
||||
prebuiltBinary = getExternalBinary(variant) |
||||
} |
||||
|
||||
return prebuiltBinary.orElseGet { |
||||
String errorMessage = """Library for $variant could not be downloaded. |
||||
|Download it from $manualDownloadUrl |
||||
|""".stripMargin() |
||||
|
||||
if (missingLibraryIsFailure) { |
||||
throw new GradleException(format(errorMessage)) |
||||
} else { |
||||
new OneTimeLogger.Static({ |
||||
error(errorMessage) |
||||
}, "${variant}-missing").log() |
||||
} |
||||
return createDirectory(tempFilePath("dummy/")) |
||||
} |
||||
} |
||||
|
||||
void setMissingLibraryIsFailure(boolean missingLibraryIsFailure) { |
||||
this.missingLibraryIsFailure = missingLibraryIsFailure |
||||
} |
||||
|
||||
void setGithubAccessToken(String githubAccessToken) { |
||||
this.githubAccessToken = githubAccessToken |
||||
} |
||||
|
||||
void setVariant(String variant) { |
||||
this.variant = variant |
||||
} |
||||
|
||||
void setUser(String user) { |
||||
this.user = user |
||||
} |
||||
|
||||
void setRepository(String repository) { |
||||
this.repository = repository |
||||
} |
||||
|
||||
void setWorkflow(String workflow) { |
||||
this.workflow = workflow |
||||
} |
||||
|
||||
void setManualDownloadUrl(String manualDownloadUrl) { |
||||
this.manualDownloadUrl = manualDownloadUrl |
||||
} |
||||
|
||||
void setBranches(List<String> branches) { |
||||
this.branches = branches |
||||
} |
||||
|
||||
void setTimeout(int timeout) { |
||||
this.timeout = timeout |
||||
} |
||||
|
||||
private Map getCacheInfo() { |
||||
if (cacheInfo == null) { |
||||
LOCK.readLock().lock() |
||||
try { |
||||
File cacheInfoFile = getCacheInfoFile() |
||||
JsonSlurper jsonParser = new JsonSlurper() |
||||
cacheInfo = jsonParser.parseText(cacheInfoFile.text) as Map |
||||
} finally { |
||||
LOCK.readLock().unlock() |
||||
} |
||||
} |
||||
return cacheInfo |
||||
} |
||||
|
||||
private File getCacheInfoFile() { |
||||
String path = preBuildPath(VERSION_INFO_FILE_NAME) |
||||
File cacheInfo = new File(path) |
||||
if (!cacheInfo.exists()) { |
||||
cacheInfo = createFile(path) |
||||
cacheInfo << "{}" |
||||
} |
||||
return cacheInfo |
||||
} |
||||
|
||||
private void writeToCache(String variantName, String timeStamp, File file) { |
||||
LOCK.writeLock().lock() |
||||
try { |
||||
Map cacheInfo = getCacheInfo() |
||||
Map entry = [timeStamp: timeStamp, path: file.absolutePath] |
||||
cacheInfo.put(variantName, entry) |
||||
getCacheInfoFile().write(JsonOutput.prettyPrint(JsonOutput.toJson(cacheInfo))) |
||||
} finally { |
||||
LOCK.writeLock().unlock() |
||||
} |
||||
} |
||||
|
||||
Optional<File> getExternalBinary(String variant) { |
||||
Tuple2<Optional<DownloadInfo>, Optional<File>> fetchResult = getBinaryDownloadUrl(variant) |
||||
Optional<DownloadInfo> downloadInfo = fetchResult.getFirst() |
||||
Optional<File> cachedFile = fetchResult.getSecond() |
||||
if (cachedFile.isPresent()) { |
||||
log("Reusing previously downloaded binary ${cachedFile.map { it.absolutePath }.orElse(null)}") |
||||
return cachedFile |
||||
} |
||||
Optional<File> downloadedFile = downloadInfo.map { |
||||
getBinaryFromUrl(variant, it.url).orElse(null) |
||||
} |
||||
|
||||
if (downloadedFile.isPresent()) { |
||||
writeToCache(variant, downloadInfo.get()?.timeStamp, downloadedFile.get()) |
||||
} else { |
||||
info("No file found for variant $variant") |
||||
} |
||||
|
||||
if (downloadedFile.isPresent()) return downloadedFile |
||||
return getCachedFile(variant) |
||||
} |
||||
|
||||
private Optional<File> getBinaryFromUrl(String variant, String url) { |
||||
File directory = createDirectory(preBuildPath(variant)) |
||||
info("Downloading binary for variant '$variant' from $url") |
||||
Optional<File> file = downloadZipFile(url, variant).map { unzip(it, directory).findFirst() }.orElse(Optional.empty()) |
||||
info("Finished download for variant '$variant'") |
||||
return file |
||||
} |
||||
|
||||
private String preBuildPath(String variant) { |
||||
return "${project.buildDir}${File.separator}$PRE_BUILD_PATH${File.separator}$variant" |
||||
} |
||||
|
||||
private Optional<ZipFile> downloadZipFile(String url, String variant) { |
||||
return fetch(url) { |
||||
File file = createFile(zipPath(variant)) |
||||
Path response = file.toPath() |
||||
Files.copy(it.getInputStream(), response, StandardCopyOption.REPLACE_EXISTING) |
||||
return new ZipFile(file) |
||||
} |
||||
} |
||||
|
||||
private String zipPath(String name) { |
||||
return tempFilePath("${name}.zip") |
||||
} |
||||
|
||||
private String tempFilePath(String name) { |
||||
return "$project.buildDir${File.separator}$TEMP_PATH${File.separator}${name}" |
||||
} |
||||
|
||||
private static Stream<File> unzip(ZipFile self, File directory) { |
||||
Collection<ZipEntry> files = self.entries().findAll { !(it as ZipEntry).directory } |
||||
return files.stream().map { |
||||
ZipEntry e = it as ZipEntry |
||||
e.name.with { fileName -> |
||||
File outputFile = createFile("${directory.path}$File.separator$fileName") |
||||
Files.copy(self.getInputStream(e), outputFile.toPath(), StandardCopyOption.REPLACE_EXISTING) |
||||
return outputFile |
||||
} |
||||
} |
||||
} |
||||
|
||||
private Tuple2<Optional<DownloadInfo>, Optional<File>> getBinaryDownloadUrl(String variantName) { |
||||
boolean isUptoDate = false |
||||
File cachedFile = null |
||||
String timeStamp = null |
||||
String artifactUrl = getLatestRun(getJson(getWorkflowsUrl())).with { |
||||
timeStamp = it.get("created_at") |
||||
Optional<String> cachedFilePath = getCachedFilePath(variantName, timeStamp) |
||||
isUptoDate = cachedFilePath.isPresent() |
||||
if (isUptoDate) { |
||||
cachedFile = new File(cachedFilePath.get()) |
||||
isUptoDate = cachedFile.exists() |
||||
} |
||||
return get("artifacts_url") as String |
||||
} |
||||
info("Latest artifact for variant '$variantName' is from $timeStamp") |
||||
if (isUptoDate) { |
||||
return new Tuple2<>(Optional.empty(), Optional.of(cachedFile)) |
||||
} |
||||
DownloadInfo downloadInfo = artifactUrl?.with { url -> |
||||
Map[] artifacts = getJson(url).get("artifacts") as Map[] |
||||
String artifactDownloadUrl = artifacts?.find { variantName == it.get("name") }?.get("url") as String |
||||
return artifactDownloadUrl?.with { |
||||
new DownloadInfo(getJson(it)?.get("archive_download_url") as String, timeStamp) |
||||
} |
||||
} |
||||
|
||||
return new Tuple2<>(Optional.ofNullable(downloadInfo), Optional.empty()) |
||||
} |
||||
|
||||
private String getWorkflowsUrl() { |
||||
return "https://api.github.com/repos/$user/$repository/actions/workflows/$workflow/runs" |
||||
} |
||||
|
||||
private Optional<String> getCachedFilePath(String variantName, String timeStamp) { |
||||
Map cacheInfo = getCacheInfo() |
||||
boolean isLatest = (cacheInfo[variantName] as Map)?.get("timeStamp") == timeStamp |
||||
if (isLatest) { |
||||
return Optional.ofNullable((cacheInfo[variantName] as Map)?.get("path") as String) |
||||
} else { |
||||
return Optional.empty() |
||||
} |
||||
} |
||||
|
||||
private Optional<File> getCachedFile(String variant) { |
||||
Map cacheInfo = getCacheInfo() |
||||
return Optional.ofNullable(cacheInfo[variant] as Map).map { |
||||
return new File(String.valueOf(it["path"])).with { |
||||
if (it.exists()) useCachedWarning.log() |
||||
it.exists() ? it : null |
||||
} |
||||
} |
||||
} |
||||
|
||||
private Map getLatestRun(Map json) { |
||||
Map[] runs = json.get("workflow_runs") as Map[] |
||||
return Optional.ofNullable(runs?.find { run -> |
||||
boolean completed = "completed" == run.get("status") |
||||
boolean success = "success" == run.get("conclusion") |
||||
boolean isCorrectBranch = branches.isEmpty() || branches.contains(run.get("head_branch")?.toString()) |
||||
return completed && success && isCorrectBranch |
||||
}).orElseGet { |
||||
log("No suitable workflow run found.") |
||||
return Collections.emptyMap() |
||||
} |
||||
} |
||||
|
||||
private Map getJson(String url) { |
||||
return fetch(url) { |
||||
JsonSlurper jsonParser = new JsonSlurper() |
||||
Map parsedJson = jsonParser.parseText(it.getInputStream().getText()) as Map |
||||
return parsedJson |
||||
}.orElse(Collections.emptyMap()) |
||||
} |
||||
|
||||
private <T> Optional<T> fetch(String url, Transformer<T, HttpURLConnection> transformer) { |
||||
info("Fetching $url") |
||||
if (isOffline()) return Optional.empty() |
||||
HttpURLConnection get = new URL(url).openConnection() as HttpURLConnection |
||||
get.setRequestMethod("GET") |
||||
if (timeout >= 0) { |
||||
get.setConnectTimeout(timeout) |
||||
} |
||||
githubAccessToken?.with { |
||||
get.setRequestProperty("Authorization", "token $it") |
||||
} |
||||
try { |
||||
def responseCode = get.getResponseCode() |
||||
if (responseCode == HttpURLConnection.HTTP_OK) { |
||||
return Optional.ofNullable(transformer.transform(get)) |
||||
} else { |
||||
log("Could not fetch $url. Response code '$responseCode'.") |
||||
} |
||||
} catch (IOException ignored) { |
||||
error(ignored.getMessage()) |
||||
} |
||||
return Optional.empty() |
||||
} |
||||
|
||||
private static File createFile(String fileName) { |
||||
File file = new File(fileName) |
||||
if (file.exists()) file.delete() |
||||
file.getParentFile().mkdirs() |
||||
file.createNewFile() |
||||
return file |
||||
} |
||||
|
||||
private static File createDirectory(String fileName) { |
||||
File file = new File(fileName) |
||||
file.mkdirs() |
||||
return file |
||||
} |
||||
|
||||
private boolean isOffline() { |
||||
return project.getGradle().startParameter.isOffline() |
||||
} |
||||
|
||||
private void info(String message) { |
||||
project.logger.info(format(message)) |
||||
} |
||||
|
||||
private void log(String message) { |
||||
project.logger.warn(format(message)) |
||||
} |
||||
|
||||
private void error(String message) { |
||||
project.logger.error(format(message)) |
||||
} |
||||
|
||||
private String format(String message) { |
||||
String pad = " " * (project.name.size() + 2) |
||||
return "${project.name}: ${message.replace("\n", "\n$pad")}" |
||||
} |
||||
|
||||
private class DownloadInfo { |
||||
protected String url |
||||
protected String timeStamp |
||||
|
||||
private DownloadInfo(String url, String timeStamp) { |
||||
this.url = url |
||||
this.timeStamp = timeStamp |
||||
} |
||||
} |
||||
} |
@ -1,33 +0,0 @@
|
||||
import dev.nokee.runtime.nativebase.OperatingSystemFamily |
||||
import dev.nokee.runtime.nativebase.TargetMachine |
||||
import org.gradle.api.GradleException |
||||
import org.gradle.api.Project |
||||
|
||||
class JniUtils { |
||||
static String asVariantName(TargetMachine targetMachine) { |
||||
String operatingSystemFamily = 'macos' |
||||
if (targetMachine.operatingSystemFamily.windows) { |
||||
operatingSystemFamily = 'windows' |
||||
} else if (targetMachine.operatingSystemFamily.linux) { |
||||
operatingSystemFamily = 'linux' |
||||
} |
||||
|
||||
String architecture = 'x86-64' |
||||
if (targetMachine.architecture.is32Bit()) { |
||||
architecture = 'x86' |
||||
} |
||||
|
||||
return "$operatingSystemFamily-$architecture" |
||||
} |
||||
|
||||
static String getLibraryFileNameFor(Project project, OperatingSystemFamily osFamily) { |
||||
if (osFamily.windows) { |
||||
return "${project.name}.dll" |
||||
} else if (osFamily.linux) { |
||||
return "lib${project.name}.so" |
||||
} else if (osFamily.macOS) { |
||||
return "lib${project.name}.dylib" |
||||
} |
||||
throw new GradleException("Unknown operating system family '${osFamily}'.") |
||||
} |
||||
} |
@ -1,45 +0,0 @@
|
||||
class OneTimeLogger { |
||||
private final Runnable logger |
||||
private boolean messageAlreadyLogged = false |
||||
|
||||
OneTimeLogger(Runnable logger) { |
||||
this.logger = logger |
||||
} |
||||
|
||||
protected boolean isLogged() { |
||||
return messageAlreadyLogged |
||||
} |
||||
|
||||
protected void setLogged(boolean logged) { |
||||
messageAlreadyLogged = logged |
||||
} |
||||
|
||||
protected void log() { |
||||
if (!isLogged()) { |
||||
logger.run() |
||||
setLogged(true) |
||||
} |
||||
} |
||||
|
||||
static class Static extends OneTimeLogger { |
||||
|
||||
private static final Map<Object, Boolean> isLogged = new HashMap<>() |
||||
|
||||
private final Object identifier |
||||
|
||||
Static(Runnable logger, Object identifier) { |
||||
super(logger) |
||||
this.identifier = identifier |
||||
} |
||||
|
||||
@Override |
||||
protected void setLogged(boolean logged) { |
||||
isLogged.put(identifier, logged) |
||||
} |
||||
|
||||
@Override |
||||
protected boolean isLogged() { |
||||
return Boolean.TRUE == isLogged.get(identifier) |
||||
} |
||||
} |
||||
} |
@ -1,72 +0,0 @@
|
||||
import dev.nokee.platform.jni.JniJarBinary |
||||
import dev.nokee.platform.jni.JniLibrary |
||||
import dev.nokee.platform.jni.JniLibraryExtension |
||||
import dev.nokee.runtime.nativebase.TargetMachine |
||||
import groovy.transform.CompileStatic |
||||
import org.gradle.api.Plugin |
||||
import org.gradle.api.Project |
||||
import org.gradle.api.Transformer |
||||
import org.gradle.api.file.CopySpec |
||||
import org.gradle.api.provider.Provider |
||||
import org.gradle.jvm.tasks.Jar |
||||
|
||||
@CompileStatic |
||||
class UberJniJarPlugin implements Plugin<Project> { |
||||
|
||||
@Override |
||||
void apply(Project project) { |
||||
project.tasks.named('jar', Jar) { task -> |
||||
configure(task) |
||||
} |
||||
} |
||||
|
||||
private static void configure(Jar task) { |
||||
def project = task.getProject() |
||||
def logger = task.getLogger() |
||||
def library = project.extensions.getByType(JniLibraryExtension) |
||||
library.binaries.withType(JniJarBinary).configureEach { |
||||
if (it.jarTask.isPresent()) it.jarTask.get()?.enabled = false |
||||
} |
||||
logger.info("${project.name}: Merging binaries into the JVM Jar.") |
||||
if (library.targetMachines.get().size() > 1) { |
||||
for (TargetMachine targetMachine : library.targetMachines.get()) { |
||||
Provider<JniLibrary> variant = library.variants |
||||
.flatMap(targetMachineOf(targetMachine)) |
||||
.map(onlyOne() as Transformer<?, ? super List<?>>) as Provider<JniLibrary> |
||||
task.into(variant.map { it.resourcePath }) { CopySpec spec -> |
||||
spec.from(variant.map { it.nativeRuntimeFiles }) |
||||
} |
||||
} |
||||
} else { |
||||
library.variants.configureEach { |
||||
task.into(it.resourcePath) { CopySpec spec -> |
||||
spec.from(it.nativeRuntimeFiles) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Filter variants that match the specified target machine. |
||||
private static Transformer<Iterable<JniLibrary>, JniLibrary> targetMachineOf(TargetMachine targetMachine) { |
||||
return new Transformer<Iterable<JniLibrary>, JniLibrary>() { |
||||
@Override |
||||
Iterable<JniLibrary> transform(JniLibrary variant) { |
||||
if (variant.targetMachine == targetMachine) { |
||||
return [variant] |
||||
} |
||||
return [] |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Ensure only a single variant is present in the collection and return the variant. |
||||
private static Transformer<JniLibrary, List<? extends JniLibrary>> onlyOne() { |
||||
return new Transformer<JniLibrary, List<? extends JniLibrary>>() { |
||||
@Override |
||||
JniLibrary transform(List<? extends JniLibrary> variants) { |
||||
assert variants.size() == 1 |
||||
return variants.first() |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,174 +0,0 @@
|
||||
import dev.nokee.platform.jni.JniLibraryExtension |
||||
import groovy.transform.CompileStatic |
||||
import org.gradle.api.Action |
||||
import org.gradle.api.Plugin |
||||
import org.gradle.api.Project |
||||
import org.gradle.api.plugins.ExtensionAware |
||||
|
||||
@CompileStatic |
||||
class UsePrebuiltBinariesWhenUnbuildablePlugin implements Plugin<Project> { |
||||
|
||||
private PrebuildBinariesExtension prebuildExtension |
||||
private GithubArtifactExtension githubArtifactExtension |
||||
|
||||
void prebuildBinaries(Action<? extends PrebuildBinariesExtension> action) { |
||||
action.execute(prebuildExtension) |
||||
} |
||||
|
||||
PrebuildBinariesExtension getPrebuildBinaries() { |
||||
return prebuildExtension |
||||
} |
||||
|
||||
@Override |
||||
void apply(Project project) { |
||||
JniLibraryExtension library = project.extensions.getByType(JniLibraryExtension) |
||||
prebuildExtension = project.extensions.create("prebuildBinaries", PrebuildBinariesExtension, this) |
||||
githubArtifactExtension = (prebuildExtension as ExtensionAware).with { |
||||
it.extensions.create("github", GithubArtifactExtension) |
||||
} |
||||
library.variants.configureEach { var -> |
||||
if (prebuildExtension.alwaysUsePrebuildArtifact || !var.sharedLibrary.buildable) { |
||||
// Try to include the library file... if available |
||||
def defaultLibraryName = JniUtils.getLibraryFileNameFor(project, var.targetMachine.operatingSystemFamily) |
||||
def variantName = JniUtils.asVariantName(var.targetMachine) |
||||
def libraryFile = project.file( |
||||
"${prebuildExtension.prebuildLibrariesFolder}/$variantName/$defaultLibraryName" |
||||
) |
||||
|
||||
if (!libraryFile.exists()) { |
||||
// No local binary provided. Try to download it from github actions. |
||||
def prebuiltBinariesTask = project.tasks.register("downloadPrebuiltBinary$variantName", DownloadPrebuiltBinaryFromGitHubAction.class) |
||||
prebuiltBinariesTask.configure { |
||||
it.githubAccessToken = githubArtifactExtension.accessToken |
||||
it.variant = variantName |
||||
it.user = githubArtifactExtension.user |
||||
it.repository = githubArtifactExtension.repository |
||||
it.workflow = githubArtifactExtension.workflow |
||||
it.manualDownloadUrl = githubArtifactExtension.manualDownloadUrl |
||||
it.branches = githubArtifactExtension.branches |
||||
it.missingLibraryIsFailure = prebuildExtension.missingLibraryIsFailure |
||||
it.timeout = githubArtifactExtension.timeout |
||||
} |
||||
var.nativeRuntimeFiles.setFrom(prebuiltBinariesTask.map { it.prebuiltBinaryFile }) |
||||
var.nativeRuntimeFiles.from(new CallableLogger({ |
||||
project.logger.warn("${project.name}: Using pre-build library from github for targetMachine $variantName.") |
||||
})) |
||||
} else { |
||||
//Use provided library. |
||||
var.nativeRuntimeFiles.setFrom(libraryFile) |
||||
var.nativeRuntimeFiles.from(new CallableLogger({ |
||||
def relativePath = project.rootProject.relativePath(libraryFile) |
||||
project.logger.warn("${project.name}: Using pre-build library $relativePath for targetMachine $variantName.") |
||||
})) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
static class PrebuildBinariesExtension { |
||||
|
||||
private String prebuildLibrariesFolder = "pre-build-libraries" |
||||
private boolean alwaysUsePrebuildArtifact = false |
||||
private boolean missingLibraryIsFailure = true |
||||
private UsePrebuiltBinariesWhenUnbuildablePlugin plugin |
||||
|
||||
PrebuildBinariesExtension(UsePrebuiltBinariesWhenUnbuildablePlugin plugin) { |
||||
this.plugin = plugin |
||||
} |
||||
|
||||
void github(Action<? extends GithubArtifactExtension> action) { |
||||
action.execute(plugin.githubArtifactExtension) |
||||
} |
||||
|
||||
void setAlwaysUsePrebuildArtifact(boolean alwaysUsePrebuildArtifact) { |
||||
this.alwaysUsePrebuildArtifact = alwaysUsePrebuildArtifact |
||||
} |
||||
|
||||
boolean getAlwaysUsePrebuildArtifact() { |
||||
return alwaysUsePrebuildArtifact |
||||
} |
||||
|
||||
String getPrebuildLibrariesFolder() { |
||||
return prebuildLibrariesFolder |
||||
} |
||||
|
||||
boolean getMissingLibraryIsFailure() { |
||||
return missingLibraryIsFailure |
||||
} |
||||
|
||||
void setPrebuildLibrariesFolder(String prebuildLibrariesFolder) { |
||||
this.prebuildLibrariesFolder = prebuildLibrariesFolder |
||||
} |
||||
|
||||
void setMissingLibraryIsFailure(boolean missingLibraryIsFailure) { |
||||
this.missingLibraryIsFailure = missingLibraryIsFailure |
||||
} |
||||
} |
||||
|
||||
static class GithubArtifactExtension { |
||||
private String user |
||||
private String repository |
||||
private String workflow |
||||
private String manualDownloadUrl |
||||
private String accessToken |
||||
private int timeout = 0 |
||||
private List<String> branches = ["master"] |
||||
|
||||
String getUser() { |
||||
return user |
||||
} |
||||
|
||||
String getRepository() { |
||||
return repository |
||||
} |
||||
|
||||
String getWorkflow() { |
||||
return workflow |
||||
} |
||||
|
||||
String getManualDownloadUrl() { |
||||
return manualDownloadUrl |
||||
} |
||||
|
||||
String getAccessToken() { |
||||
return accessToken |
||||
} |
||||
|
||||
List<String> getBranches() { |
||||
return branches |
||||
} |
||||
|
||||
int getTimeout() { |
||||
return timeout |
||||
} |
||||
|
||||
void setUser(String user) { |
||||
this.user = user |
||||
} |
||||
|
||||
void setRepository(String repository) { |
||||
this.repository = repository |
||||
} |
||||
|
||||
void setWorkflow(String workflow) { |
||||
this.workflow = workflow |
||||
} |
||||
|
||||
void setManualDownloadUrl(String manualDownloadUrl) { |
||||
this.manualDownloadUrl = manualDownloadUrl |
||||
} |
||||
|
||||
void setAccessToken(String accessToken) { |
||||
this.accessToken = accessToken |
||||
} |
||||
|
||||
void setBranches(List<String> branches) { |
||||
this.branches = branches |
||||
} |
||||
|
||||
void setTimeout(int timeout) { |
||||
this.timeout = timeout |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,210 @@
|
||||
import groovy.json.JsonOutput |
||||
import groovy.json.JsonSlurper |
||||
import org.gradle.api.DefaultTask |
||||
import org.gradle.api.GradleException |
||||
import org.gradle.api.tasks.OutputFile |
||||
import java.io.File |
||||
import java.net.HttpURLConnection |
||||
import java.net.URL |
||||
import java.nio.file.Files |
||||
import java.nio.file.StandardCopyOption |
||||
import java.util.concurrent.locks.Lock |
||||
import java.util.concurrent.locks.ReadWriteLock |
||||
import java.util.concurrent.locks.ReentrantReadWriteLock |
||||
import java.util.zip.ZipEntry |
||||
import java.util.zip.ZipFile |
||||
import javax.inject.Inject |
||||
|
||||
private typealias Json = Map<String, Any> |
||||
|
||||
@Suppress("UNCHECKED_CAST") |
||||
open class DownloadPrebuiltBinariesTask @Inject constructor( |
||||
private val variantName: String, |
||||
private val extension: PrebuiltBinariesExtension |
||||
) : DefaultTask() { |
||||
|
||||
companion object { |
||||
private val LOCK: ReadWriteLock = ReentrantReadWriteLock() |
||||
private const val VERSION_INFO_FILE_NAME = "github_artifact_versions.json" |
||||
private const val TEMP_PATH = "tmp/prebuild" |
||||
private const val PRE_BUILD_PATH = "libs/prebuild" |
||||
} |
||||
|
||||
private val githubArtifactSpec = extension.githubArtifactSpec ?: throw GradleException("Github is not configured.") |
||||
|
||||
private val isOffline |
||||
get() = project.gradle.startParameter.isOffline |
||||
|
||||
private val workflowURL |
||||
get() = with(githubArtifactSpec) { |
||||
URL("https://api.github.com/repos/$user/$repository/actions/workflows/$workflow/runs") |
||||
} |
||||
|
||||
private val prebuiltDirectoryPath = "${project.buildDir}/$PRE_BUILD_PATH/$variantName" |
||||
|
||||
private val cacheFile: File by lazy { |
||||
val cachePath = "${project.buildDir}/$PRE_BUILD_PATH/$VERSION_INFO_FILE_NAME" |
||||
fileOf(cachePath).also { it.writeText("{}") } |
||||
} |
||||
private val cache: Json by lazy { LOCK.read { cacheFile.readText().toJson() } } |
||||
|
||||
private val prebuiltBinary: File? by lazy { fetchBinaryFile() } |
||||
|
||||
@OutputFile |
||||
fun getPrebuiltBinaryFile(): File { |
||||
return prebuiltBinary ?: run { |
||||
val errorMessage = """ |
||||
Library for $variantName could not be downloaded. |
||||
Download it from ${githubArtifactSpec.manualDownloadUrl} |
||||
""".trimIndent() |
||||
if (extension.failIfLibraryIsMissing) { |
||||
throw GradleException(errorMessage) |
||||
} else { |
||||
OneTimeAction.createGlobal("$variantName-missing") { |
||||
errorLog(errorMessage) |
||||
}.execute() |
||||
} |
||||
directoryOf(tempFilePath("dummy/")) |
||||
} |
||||
} |
||||
|
||||
private fun <T> fetchFailed(message: String = ""): T? { |
||||
errorLog(message) |
||||
return null |
||||
} |
||||
|
||||
private fun fetchBinaryFile(): File? { |
||||
val run = workflowURL.getJson().latestRun |
||||
?: return fetchFailed("Could not get latest run") |
||||
val timeStamp = run["created_at"] |
||||
val cachedPathTimeStamp = cache["timeStamp"] |
||||
infoLog("Latest artifact for variant '$variantName' is from $timeStamp") |
||||
if (timeStamp == cachedPathTimeStamp) { |
||||
val cachedFile = File(cache["path"].toString()) |
||||
if (cachedFile.exists()) { |
||||
warnLog("Reusing previously downloaded binary ${cachedFile.absolutePath}") |
||||
return cachedFile |
||||
} |
||||
} |
||||
val artifactUrl = run["artifacts_url"]?.toString() |
||||
?: return fetchFailed("Could not get artifacts urls") |
||||
val artifacts = URL(artifactUrl).getJson()["artifacts"] as List<Json> |
||||
val downloadUrl = artifacts.find { variantName == it["name"] }?.get("url")?.toString() |
||||
?: return fetchFailed("Could not find matching artifact for $variantName") |
||||
val artifactDownloadUrl = URL(downloadUrl).getJson()["archive_download_url"]?.toString() |
||||
?: return fetchFailed("Could not get download url") |
||||
val artifact = downloadBinary(artifactDownloadUrl) |
||||
if (artifact != null) { |
||||
LOCK.write { |
||||
val mutableCache = cache.toMutableMap() |
||||
mutableCache["timeStamp"] = timeStamp ?: "" |
||||
mutableCache["path"] = artifact.absolutePath |
||||
cacheFile.writeText(JsonOutput.prettyPrint(JsonOutput.toJson(mutableCache))) |
||||
} |
||||
} |
||||
return artifact |
||||
} |
||||
|
||||
private fun downloadBinary(url: String): File? { |
||||
infoLog("Downloading binary for variant '$variantName' from $url") |
||||
return URL(url).fetch { |
||||
val artifact = fileOf(tempFilePath("$variantName.zip")) |
||||
Files.copy(it.inputStream, artifact.toPath(), StandardCopyOption.REPLACE_EXISTING) |
||||
infoLog("Finished download for variant '$variantName'") |
||||
ZipFile(artifact).unzip(directoryOf(prebuiltDirectoryPath)).firstOrNull() |
||||
} |
||||
} |
||||
|
||||
private val Json.latestRun: Json? |
||||
get() { |
||||
val runs = this["workflow_runs"] as List<Json> |
||||
val candidates = runs.asSequence().filter { |
||||
val completed = "completed" == it["status"] |
||||
val success = "success" == it["conclusion"] |
||||
completed && success |
||||
} |
||||
val branches = githubArtifactSpec.branches |
||||
if (branches.isEmpty()) return candidates.firstOrNull() |
||||
return branches.asSequence().mapNotNull { branch -> |
||||
candidates.find { branch == it["head_branch"] } |
||||
}.firstOrNull() |
||||
} |
||||
|
||||
private fun URL.getJson(): Json = fetch { connection -> |
||||
connection.inputStream.bufferedReader().use { it.readText() }.toJson() |
||||
} ?: emptyMap() |
||||
|
||||
private fun <T : Any> URL.fetch(transform: (HttpURLConnection) -> T?): T? { |
||||
if (isOffline) return null |
||||
infoLog("Fetching $this") |
||||
(openConnection() as HttpURLConnection).run { |
||||
requestMethod = "GET" |
||||
if (githubArtifactSpec.timeout >= 0) { |
||||
connectTimeout = githubArtifactSpec.timeout |
||||
} |
||||
githubArtifactSpec.accessToken?.also { |
||||
setRequestProperty("Authorization", "token $it") |
||||
} |
||||
return runCatching { |
||||
when (responseCode) { |
||||
HttpURLConnection.HTTP_OK -> return transform(this) |
||||
else -> error("Could not fetch $url. Response code '$responseCode'.") |
||||
} |
||||
}.getOrElse { |
||||
errorLog(it.message ?: "") |
||||
null |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun tempFilePath(name: String) = "${project.buildDir}/$TEMP_PATH/${name}" |
||||
|
||||
private fun directoryOf(fileName: String) = File(fileName).also { it.mkdirs() } |
||||
|
||||
private fun fileOf(fileName: String): File { |
||||
val file = File(fileName) |
||||
if (!file.exists()) { |
||||
file.parentFile.mkdirs() |
||||
file.createNewFile() |
||||
} |
||||
return file |
||||
} |
||||
|
||||
private fun infoLog(message: String) = project.logger.info(message.format()) |
||||
private fun warnLog(message: String) = project.logger.warn(message.format()) |
||||
private fun errorLog(message: String) = project.logger.error(message.format()) |
||||
|
||||
private fun String.format(): String { |
||||
val pad = " ".repeat(project.name.length + 2) |
||||
return "${project.name}: ${replace("\n", "\n$pad")}" |
||||
} |
||||
|
||||
private fun String.toJson(): Json = |
||||
JsonSlurper().parseText(this) as Json |
||||
|
||||
private fun <T> Lock.use(action: () -> T): T { |
||||
lock() |
||||
try { |
||||
return action() |
||||
} catch (e: Exception) { |
||||
unlock() |
||||
throw e |
||||
} finally { |
||||
unlock() |
||||
} |
||||
} |
||||
|
||||
private fun <T> ReadWriteLock.read(action: () -> T): T = readLock().use(action) |
||||
private fun <T> ReadWriteLock.write(action: () -> T): T = writeLock().use(action) |
||||
|
||||
private fun ZipFile.unzip(directory: File): Sequence<File> { |
||||
return entries().asSequence() |
||||
.map { it as ZipEntry } |
||||
.filter { !it.isDirectory } |
||||
.map { |
||||
val entryFile = fileOf("${directory.path}/${it.name}") |
||||
Files.copy(getInputStream(it), entryFile.toPath(), StandardCopyOption.REPLACE_EXISTING) |
||||
entryFile |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,23 @@
|
||||
import dev.nokee.runtime.nativebase.OperatingSystemFamily |
||||
import dev.nokee.runtime.nativebase.TargetMachine |
||||
import org.gradle.api.GradleException |
||||
import org.gradle.api.Project |
||||
|
||||
val TargetMachine.variantName: String |
||||
get() { |
||||
val osFamily = when { |
||||
operatingSystemFamily.isWindows -> "windows" |
||||
operatingSystemFamily.isLinux -> "linux" |
||||
operatingSystemFamily.isMacOS -> "macos" |
||||
else -> GradleException("Unknown operating system family '${operatingSystemFamily}'.") |
||||
} |
||||
val architecture = if (architecture.is32Bit) "x86" else "x86-64" |
||||
return "$osFamily-$architecture" |
||||
} |
||||
|
||||
fun libraryFileNameFor(project : Project, osFamily: OperatingSystemFamily) : String = when { |
||||
osFamily.isWindows -> "${project.name}.dll" |
||||
osFamily.isLinux -> "lib${project.name}.so" |
||||
osFamily.isMacOS -> "lib${project.name}.dylib" |
||||
else -> throw GradleException("Unknown operating system family '${osFamily}'.") |
||||
} |
@ -0,0 +1,37 @@
|
||||
import java.io.File |
||||
import java.util.concurrent.Callable |
||||
|
||||
class CallableAction(action: () -> Unit) : OneTimeAction(action), Callable<List<File>> { |
||||
|
||||
override fun call(): List<File> { |
||||
this.execute() |
||||
return emptyList() |
||||
} |
||||
} |
||||
|
||||
|
||||
open class OneTimeAction(private val action: () -> Unit) { |
||||
internal open var alreadyExecuted = false |
||||
|
||||
fun execute() { |
||||
if (alreadyExecuted) return |
||||
alreadyExecuted = true |
||||
action() |
||||
} |
||||
|
||||
companion object { |
||||
|
||||
private val isExecutedMap = mutableMapOf<String, Boolean>() |
||||
|
||||
fun createGlobal(name: String, action: () -> Unit): OneTimeAction { |
||||
isExecutedMap.putIfAbsent(name, false) |
||||
return object : OneTimeAction(action) { |
||||
override var alreadyExecuted |
||||
get() = isExecutedMap[name] ?: false |
||||
set(value) { |
||||
isExecutedMap[name] = value |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,53 @@
|
||||
import dev.nokee.platform.base.VariantView |
||||
import dev.nokee.platform.jni.JniJarBinary |
||||
import dev.nokee.platform.jni.JniLibrary |
||||
import dev.nokee.platform.jni.JniLibraryExtension |
||||
import org.gradle.api.Plugin |
||||
import org.gradle.api.Project |
||||
import org.gradle.api.provider.Provider |
||||
import org.gradle.jvm.tasks.Jar |
||||
import dev.nokee.runtime.nativebase.TargetMachine |
||||
|
||||
class UberJniJarPlugin : Plugin<Project> { |
||||
|
||||
override fun apply(target: Project) { |
||||
target.tasks.named("jar", Jar::class.java) { |
||||
configure(this) |
||||
} |
||||
} |
||||
|
||||
private fun configure(task: Jar) { |
||||
val project = task.project |
||||
val logger = task.logger |
||||
val library = project.extensions.getByType(JniLibraryExtension::class.java) |
||||
library.binaries.withType(JniJarBinary::class.java).configureEach { |
||||
jarTask.configure { enabled = false } |
||||
} |
||||
logger.info("${project.name}: Merging binaries into the JVM Jar.") |
||||
when (library.targetMachines.get().size) { |
||||
0 -> logger.info("No native target for project ${project.name}") |
||||
1 -> { |
||||
library.variants.configureEach { |
||||
task.into(this@configureEach.resourcePath) { |
||||
from(this@configureEach.nativeRuntimeFiles) |
||||
} |
||||
} |
||||
} |
||||
else -> { |
||||
for (targetMachine in library.targetMachines.get()) { |
||||
val variant = library.variants.withTarget(targetMachine) |
||||
task.into(variant.map { it.resourcePath }) { |
||||
from(variant.map { it.nativeRuntimeFiles }) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun VariantView<JniLibrary>.withTarget(target: TargetMachine): Provider<JniLibrary> { |
||||
return filter { it.targetMachine == target }.map { |
||||
check(it.size == 1) |
||||
it.first() |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,87 @@
|
||||
import org.gradle.api.Action |
||||
import org.gradle.api.Plugin |
||||
import org.gradle.api.Project |
||||
import dev.nokee.platform.jni.JniLibraryExtension |
||||
import dev.nokee.platform.jni.JniLibrary |
||||
import java.io.File |
||||
|
||||
class UsePrebuiltBinariesWhenUnbuildablePlugin : Plugin<Project> { |
||||
|
||||
lateinit var prebuiltExtension: PrebuiltBinariesExtension |
||||
|
||||
fun prebuiltBinaries(action: Action<PrebuiltBinariesExtension>) { |
||||
action.execute(prebuiltExtension) |
||||
} |
||||
|
||||
override fun apply(target: Project) { |
||||
prebuiltExtension = target.extensions.create("prebuiltBinaries", PrebuiltBinariesExtension::class.java) |
||||
val library = target.extensions.getByType(JniLibraryExtension::class.java) |
||||
library.variants.configureEach { |
||||
if (prebuiltExtension.alwaysUsePrebuiltArtifact || !sharedLibrary.isBuildable) { |
||||
configure(target, this) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun configure(project: Project, library: JniLibrary) { |
||||
with(prebuiltExtension) { |
||||
val defaultLibraryName = libraryFileNameFor(project, library.targetMachine.operatingSystemFamily) |
||||
val variantName = library.targetMachine.variantName |
||||
val libraryFile = project.file("$prebuiltLibrariesFolder/$variantName/$defaultLibraryName") |
||||
|
||||
if (libraryFile.exists()) { |
||||
useLocalLibrary(project, library, libraryFile, variantName) |
||||
} else { |
||||
// No local binary provided. Try to download it from github actions. |
||||
useGithubLibrary(project, library, variantName) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun useGithubLibrary(project: Project, library: JniLibrary, variantName: String) { |
||||
val prebuiltBinariesTask = project.tasks.register( |
||||
"downloadPrebuiltBinary$variantName", |
||||
DownloadPrebuiltBinariesTask::class.java, |
||||
variantName, |
||||
prebuiltExtension |
||||
) |
||||
library.nativeRuntimeFiles.setFrom(prebuiltBinariesTask.map { it.getPrebuiltBinaryFile() }) |
||||
library.nativeRuntimeFiles.from(CallableAction { |
||||
project.logger.warn( |
||||
"${project.name}: Using pre-build library from github for targetMachine $variantName." |
||||
) |
||||
}) |
||||
} |
||||
|
||||
private fun useLocalLibrary(project: Project, library: JniLibrary, libraryFile: File, variantName: String) { |
||||
library.nativeRuntimeFiles.setFrom(libraryFile) |
||||
library.nativeRuntimeFiles.from(CallableAction { |
||||
val relativePath = project.rootProject.relativePath(libraryFile) |
||||
project.logger.warn( |
||||
"${project.name}: Using pre-build library $relativePath for targetMachine $variantName." |
||||
) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
open class PrebuiltBinariesExtension { |
||||
|
||||
internal var githubArtifactSpec: GithubArtifactSpec? = null |
||||
var prebuiltLibrariesFolder: String = "pre-build-libraries" |
||||
var alwaysUsePrebuiltArtifact: Boolean = false |
||||
var failIfLibraryIsMissing: Boolean = true |
||||
|
||||
fun github(user: String, repository: String, workflow: String, action: Action<GithubArtifactSpec>) { |
||||
githubArtifactSpec = GithubArtifactSpec(user, repository, workflow).also { action.execute(it) } |
||||
} |
||||
} |
||||
|
||||
data class GithubArtifactSpec( |
||||
var user: String, |
||||
var repository: String?, |
||||
var workflow: String, |
||||
var manualDownloadUrl: String = "", |
||||
var accessToken: String? = null, |
||||
var timeout: Int = 0, |
||||
var branches: List<String> = listOf("master") |
||||
) |
Loading…
Reference in new issue