diff --git a/sofa-ark-parent/support/ark-gradle-plugin/README.md b/sofa-ark-parent/support/ark-gradle-plugin/README.md new file mode 100644 index 000000000..1599629ee --- /dev/null +++ b/sofa-ark-parent/support/ark-gradle-plugin/README.md @@ -0,0 +1,11 @@ +# Ark Gradle打包插件使用 +`sofa-ark-gradle-plugin`模块是Ark打包工具的Gradle版本实现,和Maven打包工具`sofa-ark-maven-plugin`有同样的功能,用于打包ark包和biz包。 +# 配置 +`sofa-ark-gradle-plugin` 使用 arkConfig 来进行配置。 + +# 如何使用 +1. 本地发布引用 +2. 远程仓库引入(待申请) + +参考`sofa-ark-plugin-gradle-plugin`的本地发布和引入。 +使用Gradle刷新后,如果一切正常,会在IDEA右侧Gradle任务列表中出现arkJar,具体如下: Tasks > build > arkJar,点击arkJar执行,会在指定的outputDirectory中输出ark包和biz包。 \ No newline at end of file diff --git a/sofa-ark-parent/support/ark-gradle-plugin/build.gradle b/sofa-ark-parent/support/ark-gradle-plugin/build.gradle new file mode 100644 index 000000000..575932335 --- /dev/null +++ b/sofa-ark-parent/support/ark-gradle-plugin/build.gradle @@ -0,0 +1,55 @@ +plugins { + id 'java' + id 'java-gradle-plugin' + id 'maven-publish' +} + +ext { + arkGradlePluginId = 'sofa-ark-gradle-plugin' +} + +group = 'com.alipay.sofa' +version = '1.0.0' +sourceCompatibility = '1.8' + +publishing { + publications { + maven(MavenPublication) { + from components.java + groupId = project.group + artifactId = arkGradlePluginId + version = project.version + } + } + + repositories { + maven { + mavenLocal() + } + } +} + +gradlePlugin { + plugins { + DependenciesPlugin{ + id = arkGradlePluginId + implementationClass = 'com.alipay.sofa.ark.plugin.SofaArkGradlePlugin' + } + } +} + + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + implementation 'org.ow2.asm:asm:9.4' + implementation 'org.apache.commons:commons-compress:1.26.1' + implementation 'com.alipay.sofa:sofa-ark-tools:2.2.12' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/sofa-ark-parent/support/ark-gradle-plugin/settings.gradle b/sofa-ark-parent/support/ark-gradle-plugin/settings.gradle new file mode 100644 index 000000000..56b7c384d --- /dev/null +++ b/sofa-ark-parent/support/ark-gradle-plugin/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'ark-gradle-plugin' + diff --git a/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/ArkArchiveSupport.java b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/ArkArchiveSupport.java new file mode 100644 index 000000000..1d4391f62 --- /dev/null +++ b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/ArkArchiveSupport.java @@ -0,0 +1,379 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.ark.plugin; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import org.gradle.api.GradleException; +import org.gradle.api.file.CopySpec; +import org.gradle.api.file.FileCopyDetails; +import org.gradle.api.file.FileTreeElement; +import org.gradle.api.file.RelativePath; +import org.gradle.api.internal.file.copy.CopyAction; +import org.gradle.api.internal.file.copy.CopyActionProcessingStream; +import org.gradle.api.internal.file.copy.FileCopyDetailsInternal; +import org.gradle.api.java.archives.Attributes; +import org.gradle.api.java.archives.Manifest; +import org.gradle.api.provider.Property; +import org.gradle.api.specs.Spec; +import org.gradle.api.specs.Specs; +import org.gradle.api.tasks.WorkResult; +import org.gradle.api.tasks.bundling.Jar; +import org.gradle.api.tasks.util.PatternSet; +import org.gradle.util.GradleVersion; + +import com.alipay.sofa.ark.tools.git.GitInfo; +import com.alipay.sofa.ark.tools.git.JGitParser; + +public class ArkArchiveSupport { + + private static final byte[] ZIP_FILE_HEADER = new byte[] { 'P', 'K', 3, 4 }; + + private static final String BIZ_MARKER = "com/alipay/sofa/ark/biz/mark"; + private static final String PLUGIN_MARKER = "com/alipay/sofa/ark/plugin/mark"; + private static final String CONTAINER_MARK = "com/alipay/sofa/ark/container/mark"; + + private static final Set DEFAULT_LAUNCHER_CLASSES; + + static { + Set defaultLauncherClasses = new HashSet<>(); + defaultLauncherClasses.add("org.springframework.boot.loader.JarLauncher"); + defaultLauncherClasses.add("org.springframework.boot.loader.PropertiesLauncher"); + defaultLauncherClasses.add("org.springframework.boot.loader.WarLauncher"); + DEFAULT_LAUNCHER_CLASSES = Collections.unmodifiableSet(defaultLauncherClasses); + } + + private final PatternSet requiresUnpack = new PatternSet(); + + private final PatternSet exclusions = new PatternSet(); + + private final String loaderMainClass; + + private final Spec librarySpec; + + private final Function compressionResolver; + + private final String arkVersion; + + private SofaArkGradlePluginExtension arkExtension; + + private final GitInfo gitInfo; + + private java.util.jar.Manifest arkManifest = new java.util.jar.Manifest(); + + private final List pluginFiles = new ArrayList<>(); + private final List bizFiles = new ArrayList<>(); + private List conFile = new ArrayList<>(); + + public ArkArchiveSupport(String loaderMainClass, Spec librarySpec, + Function compressionResolver, File gitDic, SofaArkGradlePluginExtension arkExtension) { + this.loaderMainClass = loaderMainClass; + this.librarySpec = librarySpec; + this.compressionResolver = compressionResolver; + this.requiresUnpack.include(Specs.satisfyNone()); + // TODO: configure as the version of sofa-ark + this.arkVersion = "2.2.14"; + this.arkExtension = arkExtension; + this.gitInfo = JGitParser.parse(gitDic); + buildArkManifest(); + } + + + public void configureBizManifest(Manifest manifest, String mainClass, String classes, String lib, String classPathIndex) { + Attributes attributes = manifest.getAttributes(); + attributes.putIfAbsent("Start-Class", mainClass); + attributes.putIfAbsent("Main-Class", mainClass); + attributes.putIfAbsent("Spring-Boot-Classes", classes); + buildModuleManifest(manifest); + + } + + public void buildArkManifest(){ + this.arkManifest.getMainAttributes().putValue("Manifest-Version", "1.0"); + this.arkManifest.getMainAttributes().putValue("Main-Class", this.loaderMainClass); + this.arkManifest.getMainAttributes().putValue("Start-Class", this.loaderMainClass); + this.arkManifest.getMainAttributes().putValue("Sofa-Ark-Version",this.arkVersion); + this.arkManifest.getMainAttributes().putValue("Ark-Container-Root","SOFA-ARK/container/"); + this.arkManifest.getMainAttributes().putValue("build-time", + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(new Date())); + + if (gitInfo != null) { + this.arkManifest.getMainAttributes().putValue("remote-origin-url", gitInfo.getRepository()); + this.arkManifest.getMainAttributes().putValue("commit-branch", gitInfo.getBranchName()); + this.arkManifest.getMainAttributes().putValue("commit-id", gitInfo.getLastCommitId()); + this.arkManifest.getMainAttributes().putValue("commit-user-name", gitInfo.getLastCommitUser()); + this.arkManifest.getMainAttributes() + .putValue("commit-user-email", gitInfo.getLastCommitEmail()); + this.arkManifest.getMainAttributes().putValue("COMMIT_TIME", gitInfo.getLastCommitDateTime()); + this.arkManifest.getMainAttributes().putValue("COMMIT_TIMESTAMP", + String.valueOf(gitInfo.getLastCommitTime())); + this.arkManifest.getMainAttributes().putValue("build-user", gitInfo.getBuildUser()); + this.arkManifest.getMainAttributes().putValue("build-email", gitInfo.getBuildEmail()); + } + } + + private void buildModuleManifest(Manifest manifest){ + Attributes attributes = manifest.getAttributes(); + attributes.putIfAbsent("Ark-Biz-Name",this.arkExtension.getBizName().get()); + attributes.putIfAbsent("Ark-Biz-Version",this.arkExtension.getBizVersion().get()); + attributes.putIfAbsent("priority",this.arkExtension.getPriority().get()); + attributes.putIfAbsent("web-context-path", this.arkExtension.getWebContextPath().get()); + attributes.putIfAbsent("deny-import-packages",joinSet(this.arkExtension.getDenyImportPackages().get())); + attributes.putIfAbsent("deny-import-classes",joinSet(this.arkExtension.getDenyImportClasses().get())); + attributes.putIfAbsent("deny-import-resources",joinSet(this.arkExtension.getDenyImportResources().get())); + attributes.putIfAbsent("inject-plugin-dependencies", joinSet(this.arkExtension.getInjectPluginDependencies().get())); + attributes.putIfAbsent("inject-export-packages",joinSet(this.arkExtension.getInjectPluginExportPackages().get())); + appendBuildInfo(manifest); + } + + private String joinSet(Set set) { + return set != null ? String.join(",", set) : ""; + } + + + private void appendBuildInfo(Manifest manifest) { + Attributes attributes = manifest.getAttributes(); + attributes.putIfAbsent("build-time", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(new Date())); + + if (gitInfo != null) { + attributes.putIfAbsent("remote-origin-url", gitInfo.getRepository()); + attributes.putIfAbsent("commit-branch", gitInfo.getBranchName()); + attributes.putIfAbsent("commit-id", gitInfo.getLastCommitId()); + attributes.putIfAbsent("commit-user-name", gitInfo.getLastCommitUser()); + attributes.putIfAbsent("commit-user-email", gitInfo.getLastCommitEmail()); + attributes.putIfAbsent("COMMIT_TIME", gitInfo.getLastCommitDateTime()); + attributes.putIfAbsent("COMMIT_TIMESTAMP", String.valueOf(gitInfo.getLastCommitTime())); + attributes.putIfAbsent("build-user", gitInfo.getBuildUser()); + attributes.putIfAbsent("build-email", gitInfo.getBuildEmail()); + } + + } + + private String determineSpringBootVersion() { + String version = getClass().getPackage().getImplementationVersion(); + return (version != null) ? version : "unknown"; + } + + public CopyAction createCopyAction(Jar jar) throws IOException { + return createCopyAction(jar, null); + } + + public CopyAction createCopyAction(Jar jar, String layerToolsLocation) throws IOException { + File bizOutput = getTargetFile(jar, this.arkExtension.getBizClassifier().get()); + File arkOutput = getTargetFile(jar, this.arkExtension.getArkClassifier().get()); + + Manifest manifest = jar.getManifest(); + boolean preserveFileTimestamps = jar.isPreserveFileTimestamps(); + Integer dirMode = getDirMode(jar); + Integer fileMode = getFileMode(jar); + boolean includeDefaultLoader = isUsingDefaultLoader(jar); + Spec requiresUnpack = this.requiresUnpack.getAsSpec(); + Spec exclusions = this.exclusions.getAsExcludeSpec(); + Spec librarySpec = this.librarySpec; + Function compressionResolver = this.compressionResolver; + String encoding = jar.getMetadataCharset(); + + CopyAction action = new ArkBizCopyAction(bizOutput,arkOutput, manifest, preserveFileTimestamps, dirMode, fileMode, + includeDefaultLoader, requiresUnpack, exclusions, librarySpec, + compressionResolver, encoding, this.arkManifest, pluginFiles, bizFiles, conFile); + + + return jar.isReproducibleFileOrder() ? new ReproducibleOrderingCopyAction(action) : action; + } + + + private File getTargetFile(Jar jar, String classifier) { + File outputDir = this.arkExtension.getOutputDirectory() + .getOrElse(jar.getDestinationDirectory().get()) + .getAsFile(); + + if (!outputDir.exists()) { + boolean created = outputDir.mkdirs(); + if (!created) { + throw new GradleException("Failed to create output directory: " + outputDir.getAbsolutePath()); + } + System.out.println("Created output directory: " + outputDir.getAbsolutePath()); + } + File targetFile = new File(outputDir, getArkBizName(jar, classifier)); + System.out.println("Target file will be created at: " + targetFile.getAbsolutePath()); + + return targetFile; + } + + private String getArkBizName(Jar jar, String classifier){ + String name = ""; + name += maybe(name, jar.getArchiveBaseName().getOrNull()); + name += maybe(name, jar.getArchiveAppendix().getOrNull()); + name += maybe(name, jar.getArchiveVersion().getOrNull()); + name += maybe(name, jar.getArchiveClassifier().getOrNull()); + name += maybe(name, classifier); + String extension = jar.getArchiveExtension().getOrNull(); + name += (isTrue(extension) ? "." + extension : ""); + return name; + } + + private Boolean isTrue(Object object){ + if (object instanceof String) { + return !((String) object).isEmpty(); + } + return false; + } + + private String maybe(String prefix, String value) { + if (isTrue(value)) { + return isTrue(prefix) ? "-".concat(value) : value; + } else { + return ""; + } + } + + + private Integer getDirMode(CopySpec copySpec) { + return getMode(copySpec, "getDirPermissions", copySpec::getDirMode); + } + + private Integer getFileMode(CopySpec copySpec) { + return getMode(copySpec, "getFilePermissions", copySpec::getFileMode); + } + + @SuppressWarnings("unchecked") + private Integer getMode(CopySpec copySpec, String methodName, Supplier fallback) { + if (GradleVersion.current().compareTo(GradleVersion.version("8.3")) >= 0) { + try { + Object filePermissions = ((Property) copySpec.getClass().getMethod(methodName).invoke(copySpec)) + .getOrNull(); + return (filePermissions != null) + ? (int) filePermissions.getClass().getMethod("toUnixNumeric").invoke(filePermissions) : null; + } + catch (Exception ex) { + throw new GradleException("Failed to get permissions", ex); + } + } + return fallback.get(); + } + + private boolean isUsingDefaultLoader(Jar jar) { + return DEFAULT_LAUNCHER_CLASSES.contains(jar.getManifest().getAttributes().get("Main-Class")); + } + + void requiresUnpack(String... patterns) { + this.requiresUnpack.include(patterns); + } + + void requiresUnpack(Spec spec) { + this.requiresUnpack.include(spec); + } + + void excludeNonZipLibraryFiles(FileCopyDetails details) { + if (this.librarySpec.isSatisfiedBy(details)) { + excludeNonZipFiles(details); + } + } + + public void excludeNonZipFiles(FileCopyDetails details) { + if (!isZip(details.getFile()) || isSofaArk(details.getFile())) { + details.exclude(); + } + } + + private boolean isZip(File file) { + try { + try (FileInputStream fileInputStream = new FileInputStream(file)) { + return isZip(fileInputStream); + } + } + catch (IOException ex) { + return false; + } + } + + private boolean isZip(InputStream inputStream) throws IOException { + for (byte headerByte : ZIP_FILE_HEADER) { + if (inputStream.read() != headerByte) { + return false; + } + } + return true; + } + + + private boolean isSofaArk(File jarFile){ + try (JarFile jar = new JarFile(jarFile)) { + for (JarEntry entry : Collections.list(jar.entries())) { + if (entry.getName().contains(BIZ_MARKER)) { + bizFiles.add(jarFile); + return true; + } else if (entry.getName().contains(PLUGIN_MARKER)) { + pluginFiles.add(jarFile); + return true; + } else if (entry.getName().contains(CONTAINER_MARK)){ + conFile.add(jarFile); + return true; + } + } + } catch (IOException e) { + + } + return false; + } + + + public void moveModuleInfoToRoot(CopySpec spec) { + spec.filesMatching("module-info.class", this::moveToRoot); + } + + public void moveToRoot(FileCopyDetails details) { + details.setRelativePath(details.getRelativeSourcePath()); + } + + /** + * {@link CopyAction} variant that sorts entries to ensure reproducible ordering. + */ + private static final class ReproducibleOrderingCopyAction implements CopyAction { + + private final CopyAction delegate; + + private ReproducibleOrderingCopyAction(CopyAction delegate) { + this.delegate = delegate; + } + + @Override + public WorkResult execute(CopyActionProcessingStream stream) { + return this.delegate.execute((action) -> { + Map detailsByPath = new TreeMap<>(); + stream.process((details) -> detailsByPath.put(details.getRelativePath(), details)); + detailsByPath.values().forEach(action::processFile); + }); + } + + } +} diff --git a/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/ArkBizCopyAction.java b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/ArkBizCopyAction.java new file mode 100644 index 000000000..9ca7963b9 --- /dev/null +++ b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/ArkBizCopyAction.java @@ -0,0 +1,527 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.ark.plugin; + +import com.alipay.sofa.ark.common.util.FileUtils; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import org.apache.commons.compress.archivers.zip.UnixStat; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; +import org.gradle.api.GradleException; +import org.gradle.api.file.FileCopyDetails; +import org.gradle.api.file.FileTreeElement; +import org.gradle.api.internal.file.copy.CopyAction; +import org.gradle.api.internal.file.copy.CopyActionProcessingStream; +import org.gradle.api.java.archives.Manifest; +import org.gradle.api.specs.Spec; +import org.gradle.api.tasks.WorkResult; +import org.gradle.api.tasks.WorkResults; +import org.gradle.util.GradleVersion; + +public class ArkBizCopyAction implements CopyAction { + + static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = OffsetDateTime.of(1980, 2, 1, 0, 0, 0, 0, ZoneOffset.UTC) + .toInstant() + .toEpochMilli(); + + private final File bizOutput; + + private final Manifest manifest; + + private final boolean preserveFileTimestamps; + + private final Integer dirMode; + + private final Integer fileMode; + + private final boolean includeDefaultLoader; + + private final Spec requiresUnpack; + + private final Spec exclusions; + + private final Spec librarySpec; + + private final Function compressionResolver; + + private final String encoding; + + private final File arkOutput; + + private String arkBootFile; + + private final java.util.jar.Manifest arkManifest; + + private List pluginFiles; + private List bizFiles; + private List conFile; + + + + ArkBizCopyAction(File bizOutput,File arkOutput, Manifest manifest, boolean preserveFileTimestamps, Integer dirMode, Integer fileMode, + boolean includeDefaultLoader, Spec requiresUnpack, + Spec exclusions, Spec librarySpec, + Function compressionResolver, String encoding, java.util.jar.Manifest arkManifest, List pluginFiles, List bizFiles, List conFile + ) throws IOException { + this.bizOutput = bizOutput; + this.arkOutput = arkOutput; + this.manifest = manifest; + this.preserveFileTimestamps = preserveFileTimestamps; + this.dirMode = dirMode; + this.fileMode = fileMode; + this.includeDefaultLoader = includeDefaultLoader; + this.requiresUnpack = requiresUnpack; + this.exclusions = exclusions; + this.librarySpec = librarySpec; + this.compressionResolver = compressionResolver; + this.encoding = encoding; + this.arkManifest = arkManifest; + this.pluginFiles = pluginFiles; + this.bizFiles = bizFiles; + this.conFile = conFile; + } + + @Override + public WorkResult execute(CopyActionProcessingStream copyActions) { + try { + writeBizArchive(copyActions); + writeArkArchive(); + return WorkResults.didWork(true); + } + catch (IOException ex) { + throw new GradleException("Failed to create " + this.bizOutput, ex); + } + } + + private void writeBizArchive(CopyActionProcessingStream copyActions) throws IOException { + OutputStream output = new FileOutputStream(this.bizOutput); + try { + writeArchive(copyActions, output); + } + finally { + closeQuietly(output); + } + } + + private void writeArkArchive() throws IOException { + OutputStream output = new FileOutputStream(this.arkOutput); + try { + writeArkArchive(output); + } + finally { + closeQuietly(output); + } + } + + private void writeArkArchive(OutputStream output) throws IOException { + ZipArchiveOutputStream zipOutput = new ZipArchiveOutputStream(output); + try { + setEncodingIfNecessary(zipOutput); + Processor1 processor = new Processor1(zipOutput); + processor.process(); + } + finally { + closeQuietly(zipOutput); + } + } + + private void writeArchive(CopyActionProcessingStream copyActions, OutputStream output) throws IOException { + ZipArchiveOutputStream zipOutput = new ZipArchiveOutputStream(output); + try { + setEncodingIfNecessary(zipOutput); + Processor processor = new Processor(zipOutput); + copyActions.process(processor::process); + processor.finish(); + } + finally { + closeQuietly(zipOutput); + } + } + + private void closeQuietly(OutputStream outputStream) { + try { + outputStream.close(); + } + catch (IOException ex) { + } + } + + private void setEncodingIfNecessary(ZipArchiveOutputStream zipOutputStream) { + if (this.encoding != null) { + zipOutputStream.setEncoding(this.encoding); + } + } + +private class Processor1{ + private final ZipArchiveOutputStream out; + + private LoaderZipEntries.WrittenEntries writtenLoaderEntries; + + private final Set writtenDirectories = new LinkedHashSet<>(); + + private final Set writtenLibraries = new LinkedHashSet<>(); + + private String arkFile; + + + Processor1(ZipArchiveOutputStream out) throws IOException { + this.out = out; + this.arkFile = conFile.get(0).getAbsolutePath(); + } + + void process() throws IOException { + writePluginJar(); + writeBootstrapEntry(); + writeArkManifest(); + writeContainer(); + writeBizJar(); + } + + void writePluginJar() throws IOException { + writeFiles(pluginFiles, "SOFA-ARK/plugin/"); + } + + void writeArkManifest() throws IOException { + ZipArchiveEntry zipArchiveEntry = new ZipArchiveEntry("META-INF/MANIFEST.MF"); + this.out.putArchiveEntry(zipArchiveEntry); + ArkBizCopyAction.this.arkManifest.write(this.out); + this.out.closeArchiveEntry(); + } + + private void writeBootstrapEntry() throws IOException { + try (JarFile jarFileSource = new JarFile(this.arkFile)){ + Enumeration entries = jarFileSource.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (entry.getName().contains("sofa-ark-archive") + || entry.getName().contains("sofa-ark-spi") + || entry.getName().contains("sofa-ark-common")) { + + JarInputStream inputStream = new JarInputStream(new BufferedInputStream( + jarFileSource.getInputStream(entry))); + writeLoaderClasses(inputStream, jarFileSource); + } + } + } catch (NullPointerException exception){ + throw new RuntimeException("No sofa-ark-all file find, please configure it"); + } + + } + + void writeContainer() throws IOException { + File file = new File(arkFile); + try( FileInputStream fileInputStream = new FileInputStream(file)) { + ZipArchiveEntry zipArchiveEntry = new ZipArchiveEntry("SOFA-ARK/container/"+ file.getName()); + writeEntry(fileInputStream, zipArchiveEntry); + } + } + + void writeBizJar() throws IOException { + bizFiles.add(bizOutput); + writeFiles(bizFiles, "SOFA-ARK/biz/"); + } + + void writeFiles(List files, String path){ + for(File file : files){ + try( FileInputStream fileInputStream = new FileInputStream(file)) { + ZipArchiveEntry zipArchiveEntry = new ZipArchiveEntry(path + file.getName()); + writeEntry(fileInputStream, zipArchiveEntry); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + private void writeEntry(FileInputStream fileInputStream, ZipArchiveEntry zipArchiveEntry) throws IOException { + this.out.putArchiveEntry(zipArchiveEntry); + byte[] buffer = new byte[1024]; + int len; + while ((len = fileInputStream.read(buffer)) > 0) { + this.out.write(buffer, 0, len); + } + this.out.closeArchiveEntry(); + } + + + + private void writeDirectory(ZipArchiveEntry entry, ZipArchiveOutputStream out) throws IOException { + out.putArchiveEntry(entry); + out.closeArchiveEntry(); + } + + private void writeClass(ZipArchiveEntry entry, ZipInputStream in, ZipArchiveOutputStream out) throws IOException { + out.putArchiveEntry(entry); + StringUtils.copyTo(in, out); + out.closeArchiveEntry(); + } + + private int getDirMode() { + return (ArkBizCopyAction.this.dirMode != null) ? ArkBizCopyAction.this.dirMode + : UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM; + } + + + private void writeLoaderClasses(JarInputStream jarInputStream, JarFile jarFileSource) throws IOException { + JarEntry entry; + while ((entry = jarInputStream.getNextJarEntry()) != null) { + if (entry.getName().endsWith(".class") + && (entry.getName().contains("com/alipay/sofa/ark/spi/archive") + || entry.getName().contains("com/alipay/sofa/ark/loader") + || entry.getName().contains("com/alipay/sofa/ark/bootstrap") + || entry.getName().contains("com/alipay/sofa/ark/common/util/StringUtils") + || entry.getName().contains("com/alipay/sofa/ark/common/util/AssertUtils") || entry + .getName().contains("com/alipay/sofa/ark/spi/constant"))) { + + ZipArchiveEntry zipArchiveEntry = new ZipArchiveEntry(entry.getName()); + this.out.putArchiveEntry(zipArchiveEntry); + + byte[] bytes = new byte[1024]; + int length; + while ((length = jarInputStream.read(bytes)) >= 0) { + this.out.write(bytes, 0, length); + } + this.out.closeArchiveEntry(); + + + } + } + jarInputStream.close(); + } + + +} + + private class Processor { + + private final ZipArchiveOutputStream out; + + private LoaderZipEntries.WrittenEntries writtenLoaderEntries; + + private final Set writtenDirectories = new LinkedHashSet<>(); + + private final Set writtenLibraries = new LinkedHashSet<>(); + + Processor(ZipArchiveOutputStream out) throws IOException { + this.out = out; + } + + void process(FileCopyDetails details) { + if (skipProcessing(details)) { + return; + } + try { + if (details.isDirectory()) { + processDirectory(details); + } else { + processFile(details); + } + } catch (IOException ex) { + throw new GradleException("Failed to add " + details + " to " + ArkBizCopyAction.this.bizOutput, ex); + } + } + + + private boolean skipProcessing(FileCopyDetails details) { + return ArkBizCopyAction.this.exclusions.isSatisfiedBy(details) + || (this.writtenLoaderEntries != null && this.writtenLoaderEntries.isWrittenDirectory(details)); + } + + private void processDirectory(FileCopyDetails details) throws IOException { + String name = details.getRelativePath().getPathString(); + ZipArchiveEntry entry = new ZipArchiveEntry(name + '/'); + prepareEntry(entry, name, getTime(details), getFileMode(details)); + this.out.putArchiveEntry(entry); + this.out.closeArchiveEntry(); + this.writtenDirectories.add(name); + } + + private void processFile(FileCopyDetails details) throws IOException { + String name = details.getRelativePath().getPathString(); + ZipArchiveEntry entry = new ZipArchiveEntry(name); + prepareEntry(entry, name, getTime(details), getFileMode(details)); + ZipCompression compression = ArkBizCopyAction.this.compressionResolver.apply(details); + if (compression == ZipCompression.STORED) { + prepareStoredEntry(details, entry); + } + this.out.putArchiveEntry(entry); + details.copyTo(this.out); + this.out.closeArchiveEntry(); + if (ArkBizCopyAction.this.librarySpec.isSatisfiedBy(details)) { + this.writtenLibraries.add(name); + } + + } + + private String getParentDirectory(String name) { + int lastSlash = name.lastIndexOf('/'); + if (lastSlash == -1) { + return null; + } + return name.substring(0, lastSlash); + } + + private void prepareEntry(ZipArchiveEntry entry, String name, Long time, int mode) throws IOException { + writeParentDirectoriesIfNecessary(name, time); + entry.setUnixMode(mode); + if (time != null) { + entry.setTime(DefaultTimeZoneOffset.INSTANCE.removeFrom(time)); + } + } + + private void writeParentDirectoriesIfNecessary(String name, Long time) throws IOException { + String parentDirectory = getParentDirectory(name); + if (parentDirectory != null && this.writtenDirectories.add(parentDirectory)) { + ZipArchiveEntry entry = new ZipArchiveEntry(parentDirectory + '/'); + prepareEntry(entry, parentDirectory, time, getDirMode()); + this.out.putArchiveEntry(entry); + this.out.closeArchiveEntry(); + } + } + + + + void finish() throws IOException { + writeArkBizMark(); + } + + private void writeArkBizMark() throws IOException { + String info = "a mark file included in sofa-ark module."; + String name = "com/alipay/sofa/ark/biz/mark"; + ZipArchiveEntry entry = new ZipArchiveEntry(name); + prepareEntry(entry, name, getTime(), getFileMode()); + this.out.putArchiveEntry(entry); + byte[] data = info.getBytes(StandardCharsets.UTF_8); + this.out.write(data, 0, data.length); + this.out.closeArchiveEntry(); + } + + private void prepareStoredEntry(FileCopyDetails details, ZipArchiveEntry archiveEntry) throws IOException { + prepareStoredEntry(details.open(), archiveEntry); + if (ArkBizCopyAction.this.requiresUnpack.isSatisfiedBy(details)) { + archiveEntry.setComment("UNPACK:" + FileUtils.sha1Hash(details.getFile())); + } + } + + private void prepareStoredEntry(InputStream input, ZipArchiveEntry archiveEntry) throws IOException { + new CrcAndSize(input).setUpStoredEntry(archiveEntry); + } + + private Long getTime() { + return getTime(null); + } + + private Long getTime(FileCopyDetails details) { + if (!ArkBizCopyAction.this.preserveFileTimestamps) { + return CONSTANT_TIME_FOR_ZIP_ENTRIES; + } + if (details != null) { + return details.getLastModified(); + } + return null; + } + + private int getDirMode() { + return (ArkBizCopyAction.this.dirMode != null) ? ArkBizCopyAction.this.dirMode + : UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM; + } + + private int getFileMode() { + return (ArkBizCopyAction.this.fileMode != null) ? ArkBizCopyAction.this.fileMode + : UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM; + } + + private int getFileMode(FileCopyDetails details) { + return (ArkBizCopyAction.this.fileMode != null) ? ArkBizCopyAction.this.fileMode + : UnixStat.FILE_FLAG | getPermissions(details); + } + + private int getPermissions(FileCopyDetails details) { + if (GradleVersion.current().compareTo(GradleVersion.version("8.3")) >= 0) { + try { + Object permissions = details.getClass().getMethod("getPermissions").invoke(details); + return (int) permissions.getClass().getMethod("toUnixNumeric").invoke(permissions); + } + catch (Exception ex) { + throw new GradleException("Failed to get permissions", ex); + } + } + return details.getMode(); + } + + + } + + + /** + * Data holder for CRC and Size. + */ + private static class CrcAndSize { + + private static final int BUFFER_SIZE = 32 * 1024; + + private final CRC32 crc = new CRC32(); + + private long size; + + CrcAndSize(InputStream inputStream) throws IOException { + try { + load(inputStream); + } + finally { + inputStream.close(); + } + } + + private void load(InputStream inputStream) throws IOException { + byte[] buffer = new byte[BUFFER_SIZE]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + this.crc.update(buffer, 0, bytesRead); + this.size += bytesRead; + } + } + + void setUpStoredEntry(ZipArchiveEntry entry) { + entry.setSize(this.size); + entry.setCompressedSize(this.size); + entry.setCrc(this.crc.getValue()); + entry.setMethod(ZipEntry.STORED); + } + + } + +} diff --git a/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/ArkJar.java b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/ArkJar.java new file mode 100644 index 000000000..f40e2c2fb --- /dev/null +++ b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/ArkJar.java @@ -0,0 +1,248 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.ark.plugin; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.concurrent.Callable; +import java.util.function.Function; +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.file.CopySpec; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.FileCopyDetails; +import org.gradle.api.file.FileTreeElement; +import org.gradle.api.internal.file.copy.CopyAction; +import org.gradle.api.provider.Property; +import org.gradle.api.specs.Spec; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.bundling.Jar; + +public class ArkJar extends Jar implements BootArchive { + + private static final String LAUNCHER = "com.alipay.sofa.ark.bootstrap.ArkLauncher"; + + private static final String CLASSES_DIRECTORY = "classes/"; + + private static final String LIB_DIRECTORY = "lib/"; + + private static final String ARK_BIZ_MARK = "com/alipay/sofa/ark/biz/mark"; + + private static final String CLASSPATH_INDEX = "classpath.idx"; + + private final ArkArchiveSupport support; + + private final CopySpec bootInfSpec; + + private final Property mainClass; + + private FileCollection classpath; + + private File gitInfo; + + private SofaArkGradlePluginExtension arkExtension; + + + /** + * Creates a new {@code BootJar} task. + */ + public ArkJar() { + Project project = getProject(); + this.gitInfo = getGitDirectory(project); + + this.arkExtension = project.getExtensions().findByType(SofaArkGradlePluginExtension.class); + this.support = new ArkArchiveSupport(LAUNCHER, new LibrarySpec(), new ZipCompressionResolver(), gitInfo, arkExtension); + + this.bootInfSpec = project.copySpec().into(""); + this.mainClass = project.getObjects().property(String.class); + configureBootInfSpec(this.bootInfSpec); + getMainSpec().with(this.bootInfSpec); + + } + + private File getGitDirectory(Project project) { + File projectDir = project.getRootDir(); + File gitFolder = new File(projectDir, ".git"); + if (gitFolder.exists() && gitFolder.isDirectory()) { + return gitFolder; + } + return new File(" "); + } + + + private void configureBootInfSpec(CopySpec bootInfSpec) { + bootInfSpec.into("", fromCallTo(this::classpathDirectories)); + bootInfSpec.into("lib", fromCallTo(this::classpathFiles)).eachFile(this.support::excludeNonZipFiles); + + this.support.moveModuleInfoToRoot(bootInfSpec); + moveMetaInfToRoot(bootInfSpec); + } + + private Iterable classpathDirectories() { + return classpathEntries(File::isDirectory); + } + + private Iterable classpathFiles() { + return classpathEntries(File::isFile); + } + + private Iterable classpathEntries(Spec filter) { + return (this.classpath != null) ? this.classpath.filter(filter) : Collections.emptyList(); + } + + private void moveMetaInfToRoot(CopySpec spec) { + spec.eachFile((file) -> { + String path = file.getRelativeSourcePath().getPathString(); + if (path.startsWith("META-INF/") && !path.equals("META-INF/aop.xml") && !path.endsWith(".kotlin_module") + && !path.startsWith("META-INF/services/")) { + this.support.moveToRoot(file); + } + }); + } + + @Override + public void copy() { + this.support.configureBizManifest(getManifest(), getMainClass().get(), CLASSES_DIRECTORY, LIB_DIRECTORY, + CLASSPATH_INDEX); + super.copy(); + } + + + @Override + protected CopyAction createCopyAction() { + try { + return this.support.createCopyAction(this); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public Property getMainClass() { + return this.mainClass; + } + + @Override + public void requiresUnpack(String... patterns) { + this.support.requiresUnpack(patterns); + } + + @Override + public void requiresUnpack(Spec spec) { + this.support.requiresUnpack(spec); + } + + @Override + public FileCollection getClasspath() { + return this.classpath; + } + + @Override + public void classpath(Object... classpath) { + FileCollection existingClasspath = this.classpath; + this.classpath = getProject().files((existingClasspath != null) ? existingClasspath : Collections.emptyList(), + classpath); + } + + @Override + public void setClasspath(Object classpath) { + this.classpath = getProject().files(classpath); + } + + @Override + public void setClasspath(FileCollection classpath) { + this.classpath = getProject().files(classpath); + } + + /** + * Returns a {@code CopySpec} that can be used to add content to the {@code BOOT-INF} + * directory of the jar. + * @return a {@code CopySpec} for {@code BOOT-INF} + * @since 2.0.3 + */ + @Internal + public CopySpec getBootInf() { + CopySpec child = getProject().copySpec(); + this.bootInfSpec.with(child); + return child; + } + + + /** + * Return the {@link ZipCompression} that should be used when adding the file + * represented by the given {@code details} to the jar. By default, any + * {@link #isLibrary(FileCopyDetails) library} is {@link ZipCompression#STORED stored} + * and all other files are {@link ZipCompression#DEFLATED deflated}. + * @param details the file copy details + * @return the compression to use + */ + protected ZipCompression resolveZipCompression(FileCopyDetails details) { + return isLibrary(details) ? ZipCompression.STORED : ZipCompression.DEFLATED; + } + + /** + * Return if the {@link FileCopyDetails} are for a library. By default any file in + * {@code BOOT-INF/lib} is considered to be a library. + * @param details the file copy details + * @return {@code true} if the details are for a library + * @since 2.3.0 + */ + protected boolean isLibrary(FileCopyDetails details) { + String path = details.getRelativePath().getPathString(); + return path.startsWith(LIB_DIRECTORY); + } + + + /** + * Syntactic sugar that makes {@link CopySpec#into} calls a little easier to read. + * @param the result type + * @param callable the callable + * @return an action to add the callable to the spec + */ + private static Action fromCallTo(Callable callable) { + return (spec) -> spec.from(callTo(callable)); + } + + /** + * Syntactic sugar that makes {@link CopySpec#from} calls a little easier to read. + * @param the result type + * @param callable the callable + * @return the callable + */ + private static Callable callTo(Callable callable) { + return callable; + } + + private final class LibrarySpec implements Spec { + @Override + public boolean isSatisfiedBy(FileCopyDetails details) { + return isLibrary(details); + } + + } + + private final class ZipCompressionResolver implements Function { + + @Override + public ZipCompression apply(FileCopyDetails details) { + return resolveZipCompression(details); + } + + } + +} diff --git a/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/ArkPluginAction.java b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/ArkPluginAction.java new file mode 100644 index 000000000..179f1962e --- /dev/null +++ b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/ArkPluginAction.java @@ -0,0 +1,249 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.ark.plugin; + +import java.io.File; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.ModuleDependency; +import org.gradle.api.attributes.AttributeContainer; +import org.gradle.api.attributes.Bundling; +import org.gradle.api.attributes.LibraryElements; +import org.gradle.api.attributes.Usage; +import org.gradle.api.file.FileCollection; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.plugins.BasePlugin; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.bundling.Jar; +import org.gradle.api.tasks.compile.JavaCompile; +import org.gradle.util.GradleVersion; + +public class ArkPluginAction implements Action { + + private static final String PARAMETERS_COMPILER_ARG = "-parameters"; + + @Override + public void execute(Project project) { + classifyJarTask(project); + configureDevelopmentOnlyConfiguration(project); + + project.afterEvaluate(this::configureArkAllArtifact); + project.afterEvaluate(this::configureBootJarTask); + + configureBuildTask(project); + + project.afterEvaluate(this::configureUtf8Encoding); + configureParametersCompilerArg(project); + configureAdditionalMetadataLocations(project); + } + + private void classifyJarTask(Project project) { + project.getTasks() + .named(JavaPlugin.JAR_TASK_NAME, Jar.class) + .configure((task) -> task.getArchiveClassifier().convention("plain")); + } + + + private void configureArkAllArtifact(Project project) { + SofaArkGradlePluginExtension arkConfig = project.getExtensions().getByType(SofaArkGradlePluginExtension.class); + + Configuration runtimeClasspath = project.getConfigurations().getByName("runtimeClasspath"); + Configuration sofaArkConfig = project.getConfigurations().maybeCreate("sofaArkConfig"); + // TODO: configure as the version of sofa-ark + Dependency arkDependency = project.getDependencies().create(SofaArkGradlePlugin.ARK_BOOTSTRAP+"2.2.14"); + ((ModuleDependency) arkDependency).setTransitive(false); + sofaArkConfig.getDependencies().add(arkDependency); + runtimeClasspath.extendsFrom(sofaArkConfig); + } + + private void configureBuildTask(Project project) { + project.getTasks() + .named(BasePlugin.ASSEMBLE_TASK_NAME) + .configure((task) -> task.dependsOn(project.getTasks().getByName("arkJar"))); + } + + private void configureBootJarTask(Project project) { + + SofaArkGradlePluginExtension arkExtension = project.getExtensions().findByType(SofaArkGradlePluginExtension.class); + Configuration configuration = project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); + + applyExclusions(configuration, arkExtension); + + SourceSet mainSourceSet = sourceSets(project).getByName(SourceSet.MAIN_SOURCE_SET_NAME); + Configuration developmentOnly = project.getConfigurations() + .getByName(SofaArkGradlePlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME); + Configuration productionRuntimeClasspath = project.getConfigurations() + .getByName(SofaArkGradlePlugin.PRODUCTION_RUNTIME_CLASSPATH_CONFIGURATION_NAME); + Callable classpath = () -> mainSourceSet.getRuntimeClasspath() + .minus((developmentOnly.minus(productionRuntimeClasspath))) + .filter(new JarTypeFileSpec()); + + + TaskProvider resolveMainClassName = ResolveMainClassName + .registerForTask(SofaArkGradlePlugin.ARK_BIZ_TASK_NAME, project, classpath); + + + project.getTasks().register(SofaArkGradlePlugin.ARK_BIZ_TASK_NAME, ArkJar.class, (arkJar) -> { + arkJar.setDescription( + "Assembles an executable jar archive containing the main classes and their dependencies."); + arkJar.setGroup(BasePlugin.BUILD_GROUP); + arkJar.classpath(classpath); + Provider manifestStartClass = project + .provider(() -> (String) arkJar.getManifest().getAttributes().get("Start-Class")); + arkJar.getMainClass() + .convention(resolveMainClassName.flatMap((resolver) -> manifestStartClass.isPresent() + ? manifestStartClass : resolveMainClassName.get().readMainClassName())); + }); + + } + + private void applyExclusions(Configuration configuration, SofaArkGradlePluginExtension arkConfig) { + for (String exclude : arkConfig.getExcludes().get()) { + String[] parts = exclude.split(":"); + // TODO: compatible with group:module:version + if (parts.length == 2) { + Map excludeMap = new HashMap<>(); + excludeMap.put("group", parts[0]); + excludeMap.put("module", parts[1]); + configuration.exclude(excludeMap); + } + } + + arkConfig.getExcludeGroupIds().get().stream() + .map(groupId -> Collections.singletonMap("group", groupId)) + .forEach(configuration::exclude); + + arkConfig.getExcludeArtifactIds().get().stream() + .map(artifactId -> Collections.singletonMap("module", artifactId)) + .forEach(configuration::exclude); + } + + + private SourceSetContainer sourceSets(Project project) { + if (GradleVersion.current().compareTo(GradleVersion.version("7.1")) < 0) { + return project.getConvention().getPlugin(org.gradle.api.plugins.JavaPluginConvention.class).getSourceSets(); + } + return project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets(); + } + + private void configureUtf8Encoding(Project evaluatedProject) { + evaluatedProject.getTasks().withType(JavaCompile.class).configureEach(this::configureUtf8Encoding); + } + + private void configureUtf8Encoding(JavaCompile compile) { + if (compile.getOptions().getEncoding() == null) { + compile.getOptions().setEncoding("UTF-8"); + } + } + + private void configureParametersCompilerArg(Project project) { + project.getTasks().withType(JavaCompile.class).configureEach((compile) -> { + List compilerArgs = compile.getOptions().getCompilerArgs(); + if (!compilerArgs.contains(PARAMETERS_COMPILER_ARG)) { + compilerArgs.add(PARAMETERS_COMPILER_ARG); + } + }); + } + + private void configureAdditionalMetadataLocations(Project project) { + project.afterEvaluate((evaluated) -> evaluated.getTasks() + .withType(JavaCompile.class) + .configureEach(this::configureAdditionalMetadataLocations)); + } + + private void configureAdditionalMetadataLocations(JavaCompile compile) { + sourceSets(compile.getProject()).stream() + .filter((candidate) -> candidate.getCompileJavaTaskName().equals(compile.getName())) + .map((match) -> match.getResources().getSrcDirs()) + .findFirst() + .ifPresent((locations) -> compile.doFirst(new AdditionalMetadataLocationsConfigurer(locations))); + } + + private void configureDevelopmentOnlyConfiguration(Project project) { + Configuration developmentOnly = project.getConfigurations() + .create(SofaArkGradlePlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME); + developmentOnly + .setDescription("Configuration for development-only dependencies such as Spring Boot's DevTools."); + Configuration runtimeClasspath = project.getConfigurations() + .getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); + Configuration productionRuntimeClasspath = project.getConfigurations() + .create(SofaArkGradlePlugin.PRODUCTION_RUNTIME_CLASSPATH_CONFIGURATION_NAME); + AttributeContainer attributes = productionRuntimeClasspath.getAttributes(); + ObjectFactory objectFactory = project.getObjects(); + attributes.attribute(Usage.USAGE_ATTRIBUTE, objectFactory.named(Usage.class, Usage.JAVA_RUNTIME)); + attributes.attribute(Bundling.BUNDLING_ATTRIBUTE, objectFactory.named(Bundling.class, Bundling.EXTERNAL)); + attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, + objectFactory.named(LibraryElements.class, LibraryElements.JAR)); + productionRuntimeClasspath.setVisible(false); + productionRuntimeClasspath.setExtendsFrom(runtimeClasspath.getExtendsFrom()); + productionRuntimeClasspath.setCanBeResolved(runtimeClasspath.isCanBeResolved()); + productionRuntimeClasspath.setCanBeConsumed(runtimeClasspath.isCanBeConsumed()); + runtimeClasspath.extendsFrom(developmentOnly); + } + + + private static final class AdditionalMetadataLocationsConfigurer implements Action { + + private final Set locations; + + private AdditionalMetadataLocationsConfigurer(Set locations) { + this.locations = locations; + } + + @Override + public void execute(Task task) { + if (!(task instanceof JavaCompile)) { + return; + } + JavaCompile compile = (JavaCompile) task; + if (hasConfigurationProcessorOnClasspath(compile)) { + configureAdditionalMetadataLocations(compile); + } + } + + private boolean hasConfigurationProcessorOnClasspath(JavaCompile compile) { + Set files = (compile.getOptions().getAnnotationProcessorPath() != null) + ? compile.getOptions().getAnnotationProcessorPath().getFiles() : compile.getClasspath().getFiles(); + return files.stream() + .map(File::getName) + .anyMatch((name) -> name.startsWith("spring-boot-configuration-processor")); + } + + private void configureAdditionalMetadataLocations(JavaCompile compile) { + compile.getOptions() + .getCompilerArgs() + .add("-Aorg.springframework.boot.configurationprocessor.additionalMetadataLocations=" + + StringUtils.collectionToCommaDelimitedString(this.locations)); + } + + } + +} diff --git a/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/BootArchive.java b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/BootArchive.java new file mode 100644 index 000000000..aa3e5c9da --- /dev/null +++ b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/BootArchive.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.ark.plugin; + +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.FileTreeElement; +import org.gradle.api.provider.Property; +import org.gradle.api.specs.Spec; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; + +public interface BootArchive extends Task { + + /** + * Returns the fully-qualified name of the application's main class. + * @return the fully-qualified name of the application's main class + * @since 2.4.0 + */ + @Input + Property getMainClass(); + + /** + * Adds Ant-style patterns that identify files that must be unpacked from the archive + * when it is launched. + * @param patterns the patterns + */ + void requiresUnpack(String... patterns); + + /** + * Adds a spec that identifies files that must be unpacked from the archive when it is + * launched. + * @param spec the spec + */ + void requiresUnpack(Spec spec); + + + /** + * Returns the classpath that will be included in the archive. + * @return the classpath + */ + @Optional + @Classpath + FileCollection getClasspath(); + + /** + * Adds files to the classpath to include in the archive. The given {@code classpath} + * is evaluated as per {@link Project#files(Object...)}. + * @param classpath the additions to the classpath + */ + void classpath(Object... classpath); + + /** + * Sets the classpath to include in the archive. The given {@code classpath} is + * evaluated as per {@link Project#files(Object...)}. + * @param classpath the classpath + * @since 2.0.7 + */ + void setClasspath(Object classpath); + + /** + * Sets the classpath to include in the archive. + * @param classpath the classpath + * @since 2.0.7 + */ + void setClasspath(FileCollection classpath); + +} diff --git a/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/DefaultTimeZoneOffset.java b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/DefaultTimeZoneOffset.java new file mode 100644 index 000000000..4de9868ee --- /dev/null +++ b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/DefaultTimeZoneOffset.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.ark.plugin; + +import java.nio.file.attribute.FileTime; +import java.util.TimeZone; + +class DefaultTimeZoneOffset { + + static final DefaultTimeZoneOffset INSTANCE = new DefaultTimeZoneOffset(TimeZone.getDefault()); + + private final TimeZone defaultTimeZone; + + DefaultTimeZoneOffset(TimeZone defaultTimeZone) { + this.defaultTimeZone = defaultTimeZone; + } + + /** + * Remove the default offset from the given time. + * @param time the time to remove the default offset from + * @return the time with the default offset removed + */ + FileTime removeFrom(FileTime time) { + return FileTime.fromMillis(removeFrom(time.toMillis())); + } + + /** + * Remove the default offset from the given time. + * @param time the time to remove the default offset from + * @return the time with the default offset removed + */ + long removeFrom(long time) { + return time - this.defaultTimeZone.getOffset(time); + } + +} diff --git a/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/JarTypeFileSpec.java b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/JarTypeFileSpec.java new file mode 100644 index 000000000..649344a19 --- /dev/null +++ b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/JarTypeFileSpec.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.ark.plugin; + +import java.io.File; +import java.util.Collections; +import java.util.Set; +import java.util.jar.JarFile; +import org.gradle.api.specs.Spec; + +class JarTypeFileSpec implements Spec { + + private static final Set EXCLUDED_JAR_TYPES = Collections.singleton("dependencies-starter"); + + @Override + public boolean isSatisfiedBy(File file) { + try (JarFile jar = new JarFile(file)) { + String jarType = jar.getManifest().getMainAttributes().getValue("Spring-Boot-Jar-Type"); + if (jarType != null && EXCLUDED_JAR_TYPES.contains(jarType)) { + return false; + } + } + catch (Exception ex) { + // Continue + } + return true; + } + +} diff --git a/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/LoaderZipEntries.java b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/LoaderZipEntries.java new file mode 100644 index 000000000..d28606d18 --- /dev/null +++ b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/LoaderZipEntries.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.ark.plugin; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; +import org.gradle.api.file.FileTreeElement; + + +/** + * Internal utility used to copy entries from the {@code spring-boot-loader.jar}. + * + * @author Andy Wilkinson + * @author Phillip Webb + * @author Scott Frederick + */ +class LoaderZipEntries { + + private final Long entryTime; + + private final int dirMode; + + private final int fileMode; + + LoaderZipEntries(Long entryTime, int dirMode, int fileMode) { + this.entryTime = entryTime; + this.dirMode = dirMode; + this.fileMode = fileMode; + } + + WrittenEntries writeTo(ZipArchiveOutputStream out) throws IOException { + WrittenEntries written = new WrittenEntries(); + try (ZipInputStream loaderJar = new ZipInputStream( + getClass().getResourceAsStream("/META-INF/loader/spring-boot-loader.jar"))) { + ZipEntry entry = loaderJar.getNextEntry(); + while (entry != null) { + if (entry.isDirectory() && !entry.getName().equals("META-INF/")) { + writeDirectory(new ZipArchiveEntry(entry), out); + written.addDirectory(entry); + } + else if (entry.getName().endsWith(".class")) { + writeClass(new ZipArchiveEntry(entry), loaderJar, out); + written.addFile(entry); + } + entry = loaderJar.getNextEntry(); + } + } + return written; + } + + private void writeDirectory(ZipArchiveEntry entry, ZipArchiveOutputStream out) throws IOException { + prepareEntry(entry, this.dirMode); + out.putArchiveEntry(entry); + out.closeArchiveEntry(); + } + + private void writeClass(ZipArchiveEntry entry, ZipInputStream in, ZipArchiveOutputStream out) throws IOException { + prepareEntry(entry, this.fileMode); + out.putArchiveEntry(entry); + copy(in, out); + out.closeArchiveEntry(); + } + + private void prepareEntry(ZipArchiveEntry entry, int unixMode) { + if (this.entryTime != null) { + entry.setTime(DefaultTimeZoneOffset.INSTANCE.removeFrom(this.entryTime)); + } + entry.setUnixMode(unixMode); + } + + private void copy(InputStream in, OutputStream out) throws IOException { + StringUtils.copyTo(in, out); + } + + + + /** + * Tracks entries that have been written. + */ + static class WrittenEntries { + + private final Set directories = new LinkedHashSet<>(); + + private final Set files = new LinkedHashSet<>(); + + private void addDirectory(ZipEntry entry) { + this.directories.add(entry.getName()); + } + + private void addFile(ZipEntry entry) { + this.files.add(entry.getName()); + } + + boolean isWrittenDirectory(FileTreeElement element) { + String path = element.getRelativePath().getPathString(); + if (element.isDirectory() && !path.endsWith(("/"))) { + path += "/"; + } + return this.directories.contains(path); + } + + Set getFiles() { + return this.files; + } + + } + +} diff --git a/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/MainClassFinder.java b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/MainClassFinder.java new file mode 100644 index 000000000..99ee09ec4 --- /dev/null +++ b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/MainClassFinder.java @@ -0,0 +1,439 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.ark.plugin; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Deque; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + + +/** + * Finds any class with a {@code public static main} method by performing a breadth first + * search. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @since 1.0.0 + */ +public abstract class MainClassFinder { + + private static final String DOT_CLASS = ".class"; + + private static final Type STRING_ARRAY_TYPE = Type.getType(String[].class); + + private static final Type MAIN_METHOD_TYPE = Type.getMethodType(Type.VOID_TYPE, STRING_ARRAY_TYPE); + + private static final String MAIN_METHOD_NAME = "main"; + + private static final FileFilter CLASS_FILE_FILTER = MainClassFinder::isClassFile; + + private static final FileFilter PACKAGE_DIRECTORY_FILTER = MainClassFinder::isPackageDirectory; + + private static boolean isClassFile(File file) { + return file.isFile() && file.getName().endsWith(DOT_CLASS); + } + + private static boolean isPackageDirectory(File file) { + return file.isDirectory() && !file.getName().startsWith("."); + } + + /** + * Find the main class from a given directory. + * @param rootDirectory the root directory to search + * @return the main class or {@code null} + * @throws IOException if the directory cannot be read + */ + public static String findMainClass(File rootDirectory) throws IOException { + return doWithMainClasses(rootDirectory, MainClass::getName); + } + + /** + * Find a single main class from the given {@code rootDirectory}. + * @param rootDirectory the root directory to search + * @return the main class or {@code null} + * @throws IOException if the directory cannot be read + */ + public static String findSingleMainClass(File rootDirectory) throws IOException { + return findSingleMainClass(rootDirectory, null); + } + + /** + * Find a single main class from the given {@code rootDirectory}. A main class + * annotated with an annotation with the given {@code annotationName} will be + * preferred over a main class with no such annotation. + * @param rootDirectory the root directory to search + * @param annotationName the name of the annotation that may be present on the main + * class + * @return the main class or {@code null} + * @throws IOException if the directory cannot be read + */ + public static String findSingleMainClass(File rootDirectory, String annotationName) throws IOException { + SingleMainClassCallback callback = new SingleMainClassCallback(annotationName); + MainClassFinder.doWithMainClasses(rootDirectory, callback); + return callback.getMainClassName(); + } + + /** + * Perform the given callback operation on all main classes from the given root + * directory. + * @param the result type + * @param rootDirectory the root directory + * @param callback the callback + * @return the first callback result or {@code null} + * @throws IOException in case of I/O errors + */ + static T doWithMainClasses(File rootDirectory, MainClassCallback callback) throws IOException { + if (!rootDirectory.exists()) { + return null; // nothing to do + } + if (!rootDirectory.isDirectory()) { + throw new IllegalArgumentException("Invalid root directory '" + rootDirectory + "'"); + } + String prefix = rootDirectory.getAbsolutePath() + "/"; + Deque stack = new ArrayDeque<>(); + stack.push(rootDirectory); + while (!stack.isEmpty()) { + File file = stack.pop(); + if (file.isFile()) { + try (InputStream inputStream = new FileInputStream(file)) { + ClassDescriptor classDescriptor = createClassDescriptor(inputStream); + if (classDescriptor != null && classDescriptor.isMainMethodFound()) { + String className = convertToClassName(file.getAbsolutePath(), prefix); + T result = callback.doWith(new MainClass(className, classDescriptor.getAnnotationNames())); + if (result != null) { + return result; + } + } + } + } + if (file.isDirectory()) { + pushAllSorted(stack, file.listFiles(PACKAGE_DIRECTORY_FILTER)); + pushAllSorted(stack, file.listFiles(CLASS_FILE_FILTER)); + } + } + return null; + } + + private static void pushAllSorted(Deque stack, File[] files) { + Arrays.sort(files, Comparator.comparing(File::getName)); + for (File file : files) { + stack.push(file); + } + } + + /** + * Find the main class in a given jar file. + * @param jarFile the jar file to search + * @param classesLocation the location within the jar containing classes + * @return the main class or {@code null} + * @throws IOException if the jar file cannot be read + */ + public static String findMainClass(JarFile jarFile, String classesLocation) throws IOException { + return doWithMainClasses(jarFile, classesLocation, MainClass::getName); + } + + /** + * Find a single main class in a given jar file. + * @param jarFile the jar file to search + * @param classesLocation the location within the jar containing classes + * @return the main class or {@code null} + * @throws IOException if the jar file cannot be read + */ + public static String findSingleMainClass(JarFile jarFile, String classesLocation) throws IOException { + return findSingleMainClass(jarFile, classesLocation, null); + } + + /** + * Find a single main class in a given jar file. A main class annotated with an + * annotation with the given {@code annotationName} will be preferred over a main + * class with no such annotation. + * @param jarFile the jar file to search + * @param classesLocation the location within the jar containing classes + * @param annotationName the name of the annotation that may be present on the main + * class + * @return the main class or {@code null} + * @throws IOException if the jar file cannot be read + */ + public static String findSingleMainClass(JarFile jarFile, String classesLocation, String annotationName) + throws IOException { + SingleMainClassCallback callback = new SingleMainClassCallback(annotationName); + MainClassFinder.doWithMainClasses(jarFile, classesLocation, callback); + return callback.getMainClassName(); + } + + /** + * Perform the given callback operation on all main classes from the given jar. + * @param the result type + * @param jarFile the jar file to search + * @param classesLocation the location within the jar containing classes + * @param callback the callback + * @return the first callback result or {@code null} + * @throws IOException in case of I/O errors + */ + static T doWithMainClasses(JarFile jarFile, String classesLocation, MainClassCallback callback) + throws IOException { + List classEntries = getClassEntries(jarFile, classesLocation); + classEntries.sort(new ClassEntryComparator()); + for (JarEntry entry : classEntries) { + try (InputStream inputStream = new BufferedInputStream(jarFile.getInputStream(entry))) { + ClassDescriptor classDescriptor = createClassDescriptor(inputStream); + if (classDescriptor != null && classDescriptor.isMainMethodFound()) { + String className = convertToClassName(entry.getName(), classesLocation); + T result = callback.doWith(new MainClass(className, classDescriptor.getAnnotationNames())); + if (result != null) { + return result; + } + } + } + } + return null; + } + + private static String convertToClassName(String name, String prefix) { + name = name.replace('/', '.'); + name = name.replace('\\', '.'); + name = name.substring(0, name.length() - DOT_CLASS.length()); + if (prefix != null) { + name = name.substring(prefix.length()); + } + return name; + } + + private static List getClassEntries(JarFile source, String classesLocation) { + classesLocation = (classesLocation != null) ? classesLocation : ""; + Enumeration sourceEntries = source.entries(); + List classEntries = new ArrayList<>(); + while (sourceEntries.hasMoreElements()) { + JarEntry entry = sourceEntries.nextElement(); + if (entry.getName().startsWith(classesLocation) && entry.getName().endsWith(DOT_CLASS)) { + classEntries.add(entry); + } + } + return classEntries; + } + + private static ClassDescriptor createClassDescriptor(InputStream inputStream) { + try { + ClassReader classReader = new ClassReader(inputStream); + ClassDescriptor classDescriptor = new ClassDescriptor(); + classReader.accept(classDescriptor, ClassReader.SKIP_CODE); + return classDescriptor; + } + catch (IOException ex) { + return null; + } + } + + private static class ClassEntryComparator implements Comparator { + + @Override + public int compare(JarEntry o1, JarEntry o2) { + Integer d1 = getDepth(o1); + Integer d2 = getDepth(o2); + int depthCompare = d1.compareTo(d2); + if (depthCompare != 0) { + return depthCompare; + } + return o1.getName().compareTo(o2.getName()); + } + + private int getDepth(JarEntry entry) { + return entry.getName().split("/").length; + } + + } + + private static class ClassDescriptor extends ClassVisitor { + + private final Set annotationNames = new LinkedHashSet<>(); + + private boolean mainMethodFound; + + ClassDescriptor() { + super(Opcodes.ASM7); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + this.annotationNames.add(Type.getType(desc).getClassName()); + return null; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + if (isAccess(access, Opcodes.ACC_PUBLIC, Opcodes.ACC_STATIC) && MAIN_METHOD_NAME.equals(name) + && MAIN_METHOD_TYPE.getDescriptor().equals(desc)) { + this.mainMethodFound = true; + } + return null; + } + + private boolean isAccess(int access, int... requiredOpsCodes) { + for (int requiredOpsCode : requiredOpsCodes) { + if ((access & requiredOpsCode) == 0) { + return false; + } + } + return true; + } + + boolean isMainMethodFound() { + return this.mainMethodFound; + } + + Set getAnnotationNames() { + return this.annotationNames; + } + + } + + /** + * Callback for handling {@link MainClass MainClasses}. + * + * @param the callback's return type + */ + interface MainClassCallback { + + /** + * Handle the specified main class. + * @param mainClass the main class + * @return a non-null value if processing should end or {@code null} to continue + */ + T doWith(MainClass mainClass); + + } + + /** + * A class with a {@code main} method. + */ + static final class MainClass { + + private final String name; + + private final Set annotationNames; + + /** + * Creates a new {@code MainClass} rather represents the main class with the given + * {@code name}. The class is annotated with the annotations with the given + * {@code annotationNames}. + * @param name the name of the class + * @param annotationNames the names of the annotations on the class + */ + MainClass(String name, Set annotationNames) { + this.name = name; + this.annotationNames = Collections.unmodifiableSet(new HashSet<>(annotationNames)); + } + + String getName() { + return this.name; + } + + Set getAnnotationNames() { + return this.annotationNames; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + MainClass other = (MainClass) obj; + return this.name.equals(other.name); + } + + @Override + public int hashCode() { + return this.name.hashCode(); + } + + @Override + public String toString() { + return this.name; + } + + } + + /** + * Find a single main class, throwing an {@link IllegalStateException} if multiple + * candidates exist. + */ + private static final class SingleMainClassCallback implements MainClassCallback { + + private final Set mainClasses = new LinkedHashSet<>(); + + private final String annotationName; + + private SingleMainClassCallback(String annotationName) { + this.annotationName = annotationName; + } + + @Override + public Object doWith(MainClass mainClass) { + this.mainClasses.add(mainClass); + return null; + } + + private String getMainClassName() { + Set matchingMainClasses = new LinkedHashSet<>(); + if (this.annotationName != null) { + for (MainClass mainClass : this.mainClasses) { + if (mainClass.getAnnotationNames().contains(this.annotationName)) { + matchingMainClasses.add(mainClass); + } + } + } + if (matchingMainClasses.isEmpty()) { + matchingMainClasses.addAll(this.mainClasses); + } + if (matchingMainClasses.size() > 1) { + throw new IllegalStateException( + "Unable to find a single main class from the following candidates " + matchingMainClasses); + } + return (matchingMainClasses.isEmpty() ? null : matchingMainClasses.iterator().next().getName()); + } + + } + +} diff --git a/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/Nullable.java b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/Nullable.java new file mode 100644 index 000000000..c66361151 --- /dev/null +++ b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/Nullable.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.ark.plugin; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import javax.annotation.Nonnull; +import javax.annotation.meta.TypeQualifierNickname; +import javax.annotation.meta.When; + +@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Nonnull(when = When.MAYBE) +@TypeQualifierNickname +public @interface Nullable { +} \ No newline at end of file diff --git a/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/ResolveMainClassName.java b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/ResolveMainClassName.java new file mode 100644 index 000000000..e85f0b75f --- /dev/null +++ b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/ResolveMainClassName.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.ark.plugin; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Objects; +import java.util.concurrent.Callable; +import org.gradle.api.DefaultTask; +import org.gradle.api.InvalidUserDataException; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.Transformer; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.RegularFile; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.plugins.BasePlugin; +import org.gradle.api.plugins.JavaApplication; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.work.DisableCachingByDefault; + + +@DisableCachingByDefault(because = "Not worth caching") +public class ResolveMainClassName extends DefaultTask { + + private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication"; + + private final RegularFileProperty outputFile; + + private final Property configuredMainClass; + + private FileCollection classpath; + + /** + * Creates a new instance of the {@code ResolveMainClassName} task. + */ + public ResolveMainClassName() { + this.outputFile = getProject().getObjects().fileProperty(); + this.configuredMainClass = getProject().getObjects().property(String.class); + } + + /** + * Returns the classpath that the task will examine when resolving the main class + * name. + * @return the classpath + */ + @Classpath + public FileCollection getClasspath() { + return this.classpath; + } + + /** + * Sets the classpath that the task will examine when resolving the main class name. + * @param classpath the classpath + */ + public void setClasspath(FileCollection classpath) { + setClasspath((Object) classpath); + } + + /** + * Sets the classpath that the task will examine when resolving the main class name. + * The given {@code classpath} is evaluated as per {@link Project#files(Object...)}. + * @param classpath the classpath + * @since 2.5.10 + */ + public void setClasspath(Object classpath) { + this.classpath = getProject().files(classpath); + } + + /** + * Returns the property for the task's output file that will contain the name of the + * main class. + * @return the output file + */ + @OutputFile + public RegularFileProperty getOutputFile() { + return this.outputFile; + } + + /** + * Returns the property for the explicitly configured main class name that should be + * used in favor of resolving the main class name from the classpath. + * @return the configured main class name property + */ + @Input + @Optional + public Property getConfiguredMainClassName() { + return this.configuredMainClass; + } + + @TaskAction + void resolveAndStoreMainClassName() throws IOException { + File outputFile = this.outputFile.getAsFile().get(); + outputFile.getParentFile().mkdirs(); + String mainClassName = resolveMainClassName(); + Files.write(outputFile.toPath(), mainClassName.getBytes(StandardCharsets.UTF_8), StandardOpenOption.WRITE, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } + + private String resolveMainClassName() { + String configuredMainClass = this.configuredMainClass.getOrNull(); + if (configuredMainClass != null) { + return configuredMainClass; + } + return getClasspath().filter(File::isDirectory) + .getFiles() + .stream() + .map(this::findMainClass) + .filter(Objects::nonNull) + .findFirst() + .orElse(""); + } + + private String findMainClass(File file) { + try { + // TODO: compatible with non-spring-boot-project + return MainClassFinder.findSingleMainClass(file, SPRING_BOOT_APPLICATION_CLASS_NAME); + } + catch (IOException ex) { + return null; + } + } + + Provider readMainClassName() { + return this.outputFile.map(new ClassNameReader()); + } + + static TaskProvider registerForTask(String taskName, Project project, + Callable classpath) { + TaskProvider resolveMainClassNameProvider = project.getTasks() + .register(taskName + "MainClassName", ResolveMainClassName.class, (resolveMainClassName) -> { + resolveMainClassName + .setDescription("Resolves the name of the application's main class for the " + taskName + " task."); + resolveMainClassName.setGroup(BasePlugin.BUILD_GROUP); + resolveMainClassName.setClasspath(classpath); + resolveMainClassName.getConfiguredMainClassName().convention(project.provider(() -> { + String javaApplicationMainClass = getJavaApplicationMainClass(project); + if (javaApplicationMainClass != null) { + return javaApplicationMainClass; + } + SofaArkGradlePluginExtension springBootExtension = project.getExtensions() + .findByType(SofaArkGradlePluginExtension.class); + return springBootExtension.getMainClass().getOrNull(); + })); + resolveMainClassName.getOutputFile() + .set(project.getLayout().getBuildDirectory().file(taskName + "MainClassName")); + }); + return resolveMainClassNameProvider; + } + + private static String getJavaApplicationMainClass(Project project) { + JavaApplication javaApplication = project.getExtensions().findByType(JavaApplication.class); + if (javaApplication == null) { + return null; + } + return javaApplication.getMainClass().getOrNull(); + } + + private static final class ClassNameReader implements Transformer { + + @Override + public String transform(RegularFile file) { + if (file.getAsFile().length() == 0) { + throw new InvalidUserDataException( + "Main class name has not been configured and it could not be resolved"); + } + Path output = file.getAsFile().toPath(); + try { + return new String(Files.readAllBytes(output), StandardCharsets.UTF_8); + } + catch (IOException ex) { + throw new RuntimeException("Failed to read main class name from '" + output + "'"); + } + } + + } + +} diff --git a/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/SofaArkGradlePlugin.java b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/SofaArkGradlePlugin.java new file mode 100644 index 000000000..a05737210 --- /dev/null +++ b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/SofaArkGradlePlugin.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.ark.plugin; + +import org.gradle.api.GradleException; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.internal.impldep.org.bouncycastle.pqc.crypto.newhope.NHSecretKeyProcessor.PartyUBuilder; +import org.gradle.util.GradleVersion; + +public class SofaArkGradlePlugin implements Plugin { + + public static final String ARK_VERSION = "2.2.14"; + public static final String ARK_BIZ_TASK_NAME = "arkJar"; + public static final String DEVELOPMENT_ONLY_CONFIGURATION_NAME = "developmentOnly"; + public static final String PRODUCTION_RUNTIME_CLASSPATH_CONFIGURATION_NAME = "productionRuntimeClasspath"; + public static final String ARK_BOOTSTRAP = "com.alipay.sofa:sofa-ark-all:"; + + @Override + public void apply(Project project) { + verifyGradleVersion(); + createAndSetExtension(project); + registerPluginActions(project); + } + + private void verifyGradleVersion() { + GradleVersion currentVersion = GradleVersion.current(); + if (currentVersion.compareTo(GradleVersion.version("6.8")) < 0) { + throw new GradleException("Spring Boot plugin requires Gradle 6.8.+ " + + "The current version is " + currentVersion); + } + } + + private void createAndSetExtension(Project project) { + project.getExtensions().create("arkConfig", SofaArkGradlePluginExtension.class, project); + } + + private void registerPluginActions(Project project) { + ArkPluginAction arkAction = new ArkPluginAction(); + arkAction.execute(project); + } + +} diff --git a/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/SofaArkGradlePluginExtension.java b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/SofaArkGradlePluginExtension.java new file mode 100644 index 000000000..d7f970f73 --- /dev/null +++ b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/SofaArkGradlePluginExtension.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.ark.plugin; + +import org.gradle.api.Project; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.SetProperty; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputDirectory; + +abstract public class SofaArkGradlePluginExtension { + + private final String ARK_JAR_PLUGIN_VERSION = "2.2.14"; + + private final Integer PRIORITY = 100; + + private final String ARK_CLASSIFIER = "ark-executable"; + + private final String FINAL_NAME = ""; + private final String BIZ_NAME = ""; + private final String BIZ_CLASSIFIER = "ark-biz"; + + private final String WEB_CONTEXT_PATH = "/"; + + private final Property mainClass; + + public SofaArkGradlePluginExtension(Project project){ + + this.mainClass = project.getObjects().property(String.class); + + getPriority().convention(project.provider(() -> PRIORITY)); + getArkClassifier().convention(project.provider(() -> ARK_CLASSIFIER)); + getFinalName().convention(project.provider(() -> FINAL_NAME)); + getBizName().convention(project.provider(() -> BIZ_NAME)); + getBizClassifier().convention(project.provider(() -> BIZ_CLASSIFIER)); + + getBizVersion().convention(project.provider(() -> project.getVersion().toString())); + getWebContextPath().convention(project.provider(()-> WEB_CONTEXT_PATH)); + + getOutputDirectory().convention(project.getLayout().getBuildDirectory().dir("libs")); + } + + public Property getMainClass() { + return this.mainClass; + } + + @OutputDirectory + abstract public DirectoryProperty getOutputDirectory(); + + abstract public Property getFinalName(); + + abstract public Property getArkClassifier(); + + abstract public Property getWebContextPath(); + + + abstract public Property getBizName(); + abstract public Property getBizClassifier(); + abstract public Property getBizVersion(); + + abstract public Property getPriority(); + + @Optional + abstract public SetProperty getExcludes(); + @Optional + abstract public SetProperty getExcludeArtifactIds(); + @Optional + abstract public SetProperty getExcludeGroupIds(); + @Optional + abstract public SetProperty getDenyImportPackages(); + @Optional + abstract public SetProperty getDenyImportClasses(); + @Optional + abstract public SetProperty getDenyImportResources(); + @Optional + abstract public SetProperty getInjectPluginDependencies(); + @Optional + abstract public SetProperty getInjectPluginExportPackages(); + +} diff --git a/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/StringUtils.java b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/StringUtils.java new file mode 100644 index 000000000..d7673be46 --- /dev/null +++ b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/StringUtils.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.ark.plugin; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collection; +import java.util.Iterator; + +public class StringUtils { + + static int copyTo(InputStream in, OutputStream out) throws IOException { + int byteCount = 0; + + int bytesRead; + for(byte[] buffer = new byte[4096]; (bytesRead = in.read(buffer)) != -1; byteCount += bytesRead) { + out.write(buffer, 0, bytesRead); + } + + out.flush(); + return byteCount; + } + + public static String collectionToCommaDelimitedString(@Nullable Collection coll) { + return collectionToDelimitedString(coll, ","); + } + + public static String collectionToDelimitedString(@Nullable Collection coll, String delim) { + return collectionToDelimitedString(coll, delim, "", ""); + } + + public static String collectionToDelimitedString( + @Nullable Collection coll, String delim, String prefix, String suffix) { + + if (CollectionUtils.isEmpty(coll)) { + return ""; + } + + int totalLength = coll.size() * (prefix.length() + suffix.length()) + (coll.size() - 1) * delim.length(); + for (Object element : coll) { + totalLength += String.valueOf(element).length(); + } + + StringBuilder sb = new StringBuilder(totalLength); + Iterator it = coll.iterator(); + while (it.hasNext()) { + sb.append(prefix).append(it.next()).append(suffix); + if (it.hasNext()) { + sb.append(delim); + } + } + return sb.toString(); + } + + + + static class CollectionUtils { + public static boolean isEmpty(@Nullable Collection collection) { + return (collection == null || collection.isEmpty()); + } + } +} diff --git a/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/ZipCompression.java b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/ZipCompression.java new file mode 100644 index 000000000..186ecc30e --- /dev/null +++ b/sofa-ark-parent/support/ark-gradle-plugin/src/main/java/com/alipay/sofa/ark/plugin/ZipCompression.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.sofa.ark.plugin; + +import java.util.zip.ZipEntry; + +public enum ZipCompression { + + /** + * The entry should be {@link ZipEntry#STORED} in the archive. + */ + STORED, + + /** + * The entry should be {@link ZipEntry#DEFLATED} in the archive. + */ + DEFLATED + +}