前言:请停止使用过时技术构建项目
官方文档中的使用devkit创建插件的部分已经不见了,如果要开新项目推荐大家一步到位直接使用gradle,这个文章的内容应该是2022年6月份左右做的,所以已经过时了,主要是记录一下之前偿还的技术债。
之前我们的插件都是手动在idea工程里右键deploy打包的,但是这样打包过于不方便,并且有可能会因为各个人的idea版本,java版本不同导致打包出现问题,所以自动化打包让流水线来做这个事是很有必要的。
相关背景调查
Idea官方的建议是通过Gradle构建插件工程,并通过官方提供的gradle插件来进行打包发布等操作,如果使用官方文档上推荐的操作,自动化打包应该是一件比较容易的事情,只需要执行gradle命令就好了
参考 https://plugins.jetbrains.com/docs/intellij/deployment.html
和官方gradle插件的 buildPlugin 命令 https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#buildplugin-task
但是在国内的网络环境下使用gradle是有一点困难的,相关的依赖也需要添加在公司的制品库中,总结一句就是使用起来相当不便,所以我们的插件工程是使用的devkit的模式https://plugins.jetbrains.com/docs/intellij/using-dev-kit.html
打包的话是通过idea的菜单,Build | Prepare Plugin Module $MODULE_NAME$ for Deployment. 来进行打包。
但是这个打包的操作是在界面上完成的,我们不可能说在流水线上安装一个完整的idea客户端,然后通过界面进行打包,所以我们需要知道这个菜单背后的实现逻辑。
原理分析
jar包分析
idea的插件包一般来说都是以一个zip包,这个zip包里面包含了一个代表插件本身工程编译出来的jar包和一些工程内部的依赖包,例如github-colpilot插件,解压之后得到的结构基本上如下图
图中的github-copilot-intellij-1.5.0.5148.jar就是插件工程编译而出来的jar包。其他的jar包都是引入的依赖。
github-copilot-intellij-1.5.0.5148.jar就和普通的java工程一样,里面装了class文件,其中有一个特殊的就是META-INF下面有idea插件的描述性文件plugin.xml。
自此我们知道了idea插件打包出来的结构,我们再来看一下Idea的 Prepare Plugin Module $MODULE_NAME$ for Deployment菜单背后的逻辑
从idea源码入手
这部分对应的源码在 https://github.com/JetBrains/intellij-community/blob/master/plugins/devkit/devkit-core/src/build/PrepareToDeployAction.java
入口是 actionPerformed
方法
核心的逻辑为每个模块都会调用的doPrepare方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| public static boolean doPrepare(Module module, List<String> errorMessages, List<String> successMessages) { String pluginName = module.getName(); String defaultPath = new File(module.getModuleFilePath()).getParent() + File.separator + pluginName; Set<Module> modules = new HashSet<>(); PluginBuildUtil.getDependencies(module, modules); modules.add(module); Set<Library> libs = new HashSet<>(); for (Module dep : modules) { PluginBuildUtil.getLibraries(dep, libs); }
Map<Module, String> jpsModules = collectJpsPluginModules(module); modules.removeAll(jpsModules.keySet());
boolean isZip = !libs.isEmpty() || !jpsModules.isEmpty(); String oldPath = defaultPath + (isZip ? JAR_EXTENSION : ZIP_EXTENSION); File oldFile = new File(oldPath); if (oldFile.exists()) { String message = DevKitBundle.message("suggest.to.delete", oldPath), title = DevKitBundle.message("info.message"); if (Messages.showYesNoDialog(module.getProject(), message, title, Messages.getInformationIcon()) == Messages.YES) { FileUtil.delete(oldFile); } }
String dstPath = defaultPath + (isZip ? ZIP_EXTENSION : JAR_EXTENSION); File dstFile = new File(dstPath); return clearReadOnly(module.getProject(), dstFile) && ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> { ProgressIndicator progressIndicator = ProgressManager.getInstance().getProgressIndicator(); if (progressIndicator != null) { progressIndicator.setText(DevKitBundle.message("prepare.for.deployment.task.progress")); progressIndicator.setIndeterminate(true); } try { File jarFile = preparePluginsJar(module, modules); if (isZip) { try { processLibrariesAndJpsPlugins(jarFile, dstFile, pluginName, libs, jpsModules); } finally { FileUtil.delete(jarFile); } } else { FileUtil.rename(jarFile, dstFile); } LocalFileSystem.getInstance().refreshIoFiles(Collections.singleton(dstFile), true, false, null); successMessages.add(DevKitBundle.message("saved.message", isZip ? 1 : 2, pluginName, dstPath)); } catch (IOException e) { errorMessages.add(e.getMessage() + "\n(" + dstPath + ")"); } }, DevKitBundle.message("prepare.for.deployment.task", pluginName), true, module.getProject()); }
|
可以看到在这个方法里面,首先收集了所有的模块和依赖,如果没有只有单个工程,没有模块和依赖,那么最终的输出结果就是一个单独的jar包,如果有模块和依赖,则将编译后的代码输出为一个jar包,并将所有的依赖jar包和这个依赖合并为一个zip包,最终组成插件的安装包。
而在 preparePluginsJar 方法中,则是创建jar包,并将每个模块编译出来的 class 文件和插件的配置文件 plugin.xml 复制进jar包中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| private static File preparePluginsJar(Module module, Set<Module> modules) throws IOException { PluginBuildConfiguration configuration = PluginBuildConfiguration.getInstance(module); Manifest manifest = createOrFindManifest(configuration); return jarModulesOutput(modules, manifest, configuration != null ? configuration.getPluginXmlPath() : null); }
private static File jarModulesOutput(Set<Module> modules, @Nullable Manifest manifest, @Nullable String pluginXmlPath) throws IOException { File tempFile = FileUtil.createTempFile(TEMP_PREFIX, JAR_EXTENSION);
try (Compressor.Jar jar = new Compressor.Jar(tempFile)) { FileTypeManager manager = FileTypeManager.getInstance(); Set<String> uniqueEntries = new HashSet<>(); jar.filter((entryName, file) -> !manager.isFileIgnored(PathUtil.getFileName(entryName)) && uniqueEntries.add(entryName));
if (manifest != null) { jar.addManifest(manifest); }
for (Module module : modules) { CompilerModuleExtension extension = CompilerModuleExtension.getInstance(module); if (extension != null) { VirtualFile outputPath = extension.getCompilerOutputPath(); if (outputPath != null) { jar.addDirectory(new File(outputPath.getPath())); } } } if (pluginXmlPath != null) { jar.addFile(PluginDescriptorConstants.PLUGIN_XML_PATH, new File(pluginXmlPath)); } }
return tempFile; }
|
如果是需要输出为zip包的化则执行 processLibrariesAndJpsPlugins 方法
理解了这个逻辑,我们就可以写自己的自动化脚本了。我们的工程结构比较奇葩,由于官方不支持maven的结构创建插件工程,所以我们创建了一个插件工程,然后创建了一堆maven模块来使用需要的依赖包,这一堆的maven模块是通过idea的Project Structure手动进行管理的(所以配置环境很容易爆炸)。我们还在sdk配置中添加了一些idea官方的扩展包,
这些包并不在普通的java运行环境中,所以我们在编译的时候需要把这些包添加classpath中,但是在插件运行时,这些jar包实际上是idea环境自带的,我们只是在打包的时候需要,并不需要添加到最终的class里面。
根据之前的逻辑,我们需要做的事情有以下几点
- 将 maven 中的依赖和 idea sdk 中的 jar 包一起联合编译我们的 java 文件为 class
- 将编译好的 class 还有 resources 里面的静态文件打包成插件本身的 Jar 包
- 完成以上步骤之后,将插件本身的 Jar 包和 Maven 依赖的 Jar 包一起打包成最终的插件包 (zip)
我们使用ant作为工具来构建我们的插件
首先我们需要准备好idea的SDK,maven的依赖,然后将这些依赖添加进 classpath 中,编译我们的 java 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| <taskdef uri="antlib:org.apache.maven.resolver.ant" resource="org/apache/maven/resolver/ant/antlib.xml"> <classpath> <fileset dir="." includes="maven-resolver-ant-tasks-*uber.jar"/> </classpath> </taskdef>
<target name="compile" depends="clean"> <resolver:resolve> <resolver:dependencies pomRef="pom"/> <resolver:path refid="cp.compile.main" classpath="compile"/> </resolver:resolve>
<fileset id="download.jar" dir="${build.temp.dir}/local-repo"> <include name="**/*.jar"/> </fileset>
<path id="runtime-classpath"> <fileset id="idea.lib1" dir="${idea.dir}/lib"> <include name="**/*"/> </fileset> <fileset id="idea.lib2" dir="${idea.dir}/plugins"> <include name="**/*.jar"/> </fileset> <fileset refid="download.jar"/> <fileset dir="lib"/> </path>
<manifestclasspath property="manifest.classpath" maxparentlevels="10" jarfile="${classpath-compile.jar}"> <classpath refid="runtime-classpath" /> </manifestclasspath>
<jar destfile="${classpath-compile.jar}"> <manifest> <attribute name="Class-Path" value="${manifest.classpath}"/> </manifest> </jar>
<javac srcdir="src/main/java" destdir="${build.temp.dir}/classes" includeAntRuntime="false" source="8" target="8" encoding="UTF-8" fork="true"> <classpath> <pathelement location="${classpath-compile.jar}" /> </classpath> </javac> </target>
|
然后将所有的静态资源,依赖的 Jar 包复制到对应的目录中
1 2 3 4 5 6 7 8 9
| <copy todir="${build.plugin.dir}"> <fileset dir="lib"/> <fileset refid="download.jar"/> <mapper type="flatten"/> </copy> <copy todir="${build.temp.dir}/classes"> <fileset dir="src/main/resources"/> <fileset dir="../resources"/> </copy>
|
把编译好的文件和静态资源打包成 Jar 包,并最终和依赖一同打包成插件的 zip 包
1 2 3 4 5 6 7
| <target name="package" depends="compile"> <jar destfile="${build.plugin.dir}/plugin.jar" basedir="${build.temp.dir}/classes"/> <zip destfile="../plugin.zip" basedir="${build.plugin.zip}"/> <resolver:artifacts id="output"> <artifact file="${build.plugin.dir}/plugin.jar"/> </resolver:artifacts> </target>
|
相关代码参考
https://github.com/JetBrains/intellij-community/blob/master/plugins/devkit/devkit-core/src/build/PrepareAllToDeployAction.java
https://github.com/JetBrains/intellij-community/blob/master/plugins/devkit/devkit-core/src/build/PrepareToDeployAction.java
评论