├── .java-version ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── settings.gradle.kts ├── .idea ├── .gitignore ├── kotlinc.xml ├── vcs.xml ├── misc.xml └── gradle.xml ├── gradle.properties ├── .gitignore ├── src └── main │ ├── resources │ └── META-INF │ │ ├── pluginIcon.svg.bak.svg │ │ ├── plugin.xml │ │ └── pluginIcon.svg │ └── kotlin │ └── org │ └── le1a │ └── jarlibsconsolidator │ └── AddJarDependenciesAction.kt ├── .run └── Run IDE with Plugin.run.xml ├── gradlew.bat ├── README.md └── gradlew /.java-version: -------------------------------------------------------------------------------- 1 | 17 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Le1a/JarLibsConsolidator/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | gradlePluginPortal() 5 | } 6 | } 7 | 8 | rootProject.name = "JarLibsConsolidator" -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 默认忽略的文件 2 | /shelf/ 3 | /workspace.xml 4 | # 基于编辑器的 HTTP 客户端请求 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=../../gradle-8.11.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib 2 | kotlin.stdlib.default.dependency=false 3 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 4 | org.gradle.configuration-cache=true 5 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html 6 | org.gradle.caching=true 7 | 8 | # ?????TLS???????SSL???? 9 | systemProp.javax.net.ssl.enabledProtocols=TLSv1.2,TLSv1.3 -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | gradle-8.11.1-bin.zip 8 | 9 | ### IntelliJ IDEA ### 10 | .idea/modules.xml 11 | .idea/jarRepositories.xml 12 | .idea/compiler.xml 13 | .idea/libraries/ 14 | *.iws 15 | *.iml 16 | *.ipr 17 | out/ 18 | !**/src/main/**/out/ 19 | !**/src/test/**/out/ 20 | 21 | ### IntelliJ Platform ### 22 | .intellijPlatform 23 | 24 | ### Eclipse ### 25 | .apt_generated 26 | .classpath 27 | .factorypath 28 | .project 29 | .settings 30 | .springBeans 31 | .sts4-cache 32 | bin/ 33 | !**/src/main/**/bin/ 34 | !**/src/test/**/bin/ 35 | 36 | ### NetBeans ### 37 | /nbproject/private/ 38 | /nbbuild/ 39 | /dist/ 40 | /nbdist/ 41 | /.nb-gradle/ 42 | 43 | ### VS Code ### 44 | .vscode/ 45 | 46 | ### Mac OS ### 47 | .DS_Store 48 | 49 | ### 其他 ### 50 | GEMINI.md 51 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg.bak.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.run/Run IDE with Plugin.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## JarLibsConsolidator 2 | 3 | 一键收集并合并项目中的 JAR 依赖,统一输出到 `all-in-one` 目录,并自动添加为项目库,挂载到所有模块。 4 | 5 | ![Kotlin](https://img.shields.io/badge/Kotlin-1.9.25-7F52FF?logo=kotlin) ![Gradle](https://img.shields.io/badge/Gradle-8.x-02303A?logo=gradle) ![IntelliJ%20Platform](https://img.shields.io/badge/IntelliJ%20Platform-241--251.*-000?logo=intellijidea) ![JDK](https://img.shields.io/badge/JDK-17-5382A1) 6 | 7 | ### 📺 演示视频 8 | 9 |
10 | 11 | JarLibsConsolidator Plugin Demo 12 | 13 |
14 |

🎬 点击播放按钮观看完整演示 | 在新窗口打开

15 |
16 | 17 | ### 背景与动机 18 | 19 | 在进行代码审计工作时,我们经常需要分析已编译打包的 JAR 源码项目。然而,IntelliJ IDEA 往往无法自动识别所有的依赖关系,特别是在分析国产开源项目时更为明显。过去我一直使用以下命令来手动收集依赖: 20 | 21 | ```bash 22 | cp `find ./ -name "*.jar"` ./all-in-one 23 | ``` 24 | 25 | 但每次操作都颇为繁琐。 26 | 27 | 为了提高工作效率,我开发了这个插件,让依赖收集变得简单高效——只需右键点击,即可完成所有 JAR 文件的收集、整理和项目库配置。 28 | 29 | ### 特性 30 | - **一键操作**:右键项目 → 选择"**一键添加依赖**"。 31 | - **智能扫描**:递归扫描 `.jar`,跳过常见目录(如 `node_modules`、`target`、`build`、`.gradle`、`.mvn` 等)。 32 | - **重名处理**:自动对同名 jar 加后缀去重(如 `x.jar` → `x_2.jar`)。 33 | - **统一管理**:复制到 `all-in-one/`,创建项目级库 `all-in-one` 并添加至所有模块依赖。 34 | - **版本兼容**:适配 2024.1–2025.1+(build `241`–`251.*`)线程模型与 API。 35 | 36 | ### 前置依赖 37 | 38 | 由于 Gradle 分发包体积较大(130MB+),需要手动下载并放置到项目根目录: 39 | 40 | ```bash 41 | # 下载 Gradle 8.11.1 分发包 42 | wget https://mirrors.cloud.tencent.com/gradle/gradle-8.11.1-bin.zip 43 | 44 | # 或者使用 curl 45 | curl -O https://mirrors.cloud.tencent.com/gradle/gradle-8.11.1-bin.zip 46 | 47 | # 确保文件位于项目根目录 48 | ls gradle-8.11.1-bin.zip 49 | ``` 50 | 51 | **注意**:该文件已被 `.gitignore` 排除,不会被提交到版本控制中。 52 | 53 | ### 快速上手 54 | 1) **插件已上传至JetBrains Marketplace,可直接在IDEA插件市场搜索JarLibsConsolidator进行安装** 55 | 56 | 2) 本地运行(开发/体验) 57 | 58 | ```bash 59 | ./gradlew runIde 60 | ``` 61 | 62 | 3) 打包安装(生成可安装的 zip) 63 | 64 | ```bash 65 | ./gradlew buildPlugin 66 | # 产物:build/distributions/JarLibsConsolidator-.zip 67 | # IDE 中安装:Settings/Preferences → Plugins → ⚙ → Install Plugin from Disk… 68 | ``` 69 | 或者直接用Releases里我打包好的jar包,在IDEA插件选择本地磁盘安装即可。 70 | 71 | ### 使用 72 | - 在 Project 视图中右键项目根目录或任意目录 → 选择“**一键添加依赖**”。 73 | - 若 `all-in-one/` 已存在,会提示是否删除并重建。 74 | - 完成后可在项目结构的 `Libraries` 看到 `all-in-one`,并已挂载至所有模块。 75 | 76 | ### 工作原理(简述) 77 | 1. 遍历工程目录收集所有 `.jar` 文件(跳过常见无关目录)。 78 | 2. 复制到 `all-in-one/`,处理重名冲突。 79 | 3. 创建/刷新项目库 `all-in-one`,将 jar 作为 `CLASSES` 根添加,并依附到所有模块。 80 | 81 | ### 兼容性与要求 82 | - **IDE**:IntelliJ IDEA 2024.1 – 2025.1+(build `241`–`251.*`) 83 | - **JDK**:17 84 | - **运行时插件**:`com.intellij.java`(已通过平台打包) 85 | 86 | ### 常见问答 87 | - **会修改源码吗?** 不会,仅复制 jar 并写入项目库配置。 88 | - **扫描很慢怎么办?** 建议在项目根执行,插件已默认跳过体量较大的常见目录;也可在更小的子目录执行。 89 | 90 | ### 开发 91 | ```bash 92 | # 验证插件 93 | ./gradlew verifyPlugin 94 | 95 | # 本地运行沙箱 IDE 96 | ./gradlew runIde 97 | 98 | # 构建可分发包 99 | ./gradlew buildPlugin 100 | ``` 101 | 102 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.le1a.JarLibsConsolidator 5 | 6 | 8 | JarLibsConsolidator 9 | 10 | 11 | 12 | 13 | 14 | Le1a 15 | 16 | 19 |
21 | 22 | Features:
23 | • One-click operation: Right-click project → Select "Add Dependencies"
24 | • Smart scanning: Recursively scans .jar files, skipping common directories (node_modules, target, build, .gradle, .mvn, etc.)
25 | • Duplicate handling: Automatically renames duplicate JARs with suffixes (x.jar → x_2.jar)
26 | • Unified management: Copies to all-in-one/ directory and creates project-level library
27 | • Version compatibility: Supports IntelliJ IDEA 2024.1–2025.1+ (build 241–251.*)

28 | 29 | 中文说明:
30 | 一键收集项目中所有jar包依赖的工具插件。遍历项目目录收集所有jar文件,复制到all-in-one文件夹,并自动添加为项目库。

31 | 32 | Project URL: https://github.com/Le1a/JarLibsConsolidator 33 | ]]>
34 | 35 | 37 | com.intellij.modules.platform 38 | 39 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 51 | 52 | 53 | 54 |
-------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /src/main/kotlin/org/le1a/jarlibsconsolidator/AddJarDependenciesAction.kt: -------------------------------------------------------------------------------- 1 | package org.le1a.jarlibsconsolidator 2 | 3 | import com.intellij.openapi.actionSystem.AnAction 4 | import com.intellij.openapi.actionSystem.AnActionEvent 5 | import com.intellij.openapi.application.ApplicationInfo 6 | import com.intellij.openapi.application.ApplicationManager 7 | import com.intellij.openapi.application.WriteAction 8 | import com.intellij.openapi.module.ModuleManager 9 | import com.intellij.openapi.progress.ProgressIndicator 10 | import com.intellij.openapi.progress.ProgressManager 11 | import com.intellij.openapi.progress.Task 12 | import com.intellij.openapi.project.Project 13 | import com.intellij.openapi.roots.LibraryOrderEntry 14 | import com.intellij.openapi.roots.ModuleRootManager 15 | import com.intellij.openapi.roots.OrderRootType 16 | import com.intellij.openapi.roots.libraries.Library 17 | import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar 18 | import com.intellij.openapi.ui.Messages 19 | import com.intellij.openapi.vfs.LocalFileSystem 20 | import com.intellij.openapi.vfs.VirtualFile 21 | import com.intellij.openapi.vfs.JarFileSystem 22 | import java.io.File 23 | import java.nio.file.Files 24 | import java.nio.file.StandardCopyOption 25 | 26 | /** 27 | * 一键添加jar依赖的Action类 28 | * 兼容多个IDEA版本 (243.x - 251.x+) 29 | */ 30 | class AddJarDependenciesAction : AnAction() { 31 | 32 | // 版本检测:2025.1 对应 build 251 33 | private val isNewThreadingModel: Boolean by lazy { 34 | val buildNumber = ApplicationInfo.getInstance().build.baselineVersion 35 | buildNumber >= 251 // 2025.1及以上版本使用新的线程模型 36 | } 37 | 38 | override fun actionPerformed(e: AnActionEvent) { 39 | val project = e.project ?: return 40 | 41 | // 获取项目根目录 42 | val basePath = project.basePath ?: run { 43 | showError(project, "无法获取项目根目录") 44 | return 45 | } 46 | 47 | val allInOneDir = File(basePath, "all-in-one") 48 | 49 | // 检查all-in-one文件夹是否已存在 50 | if (allInOneDir.exists()) { 51 | val result = Messages.showYesNoDialog( 52 | project, 53 | "all-in-one文件夹已存在,是否删除并重新创建?\n" + 54 | "点击'是'将删除现有文件夹及其内容\n" + 55 | "点击'否'将取消操作", 56 | "文件夹已存在", 57 | "删除并重新创建", 58 | "取消", 59 | Messages.getQuestionIcon() 60 | ) 61 | 62 | if (result != Messages.YES) { 63 | return // 用户选择取消 64 | } 65 | 66 | // 删除现有文件夹 67 | try { 68 | allInOneDir.deleteRecursively() 69 | } catch (e: Exception) { 70 | showError(project, "无法删除现有文件夹: ${e.message}") 71 | return 72 | } 73 | } 74 | 75 | ProgressManager.getInstance().run(object : Task.Backgroundable(project, "正在收集jar依赖...", true) { 76 | override fun run(indicator: ProgressIndicator) { 77 | try { 78 | indicator.text = "正在扫描jar文件..." 79 | indicator.fraction = 0.1 80 | 81 | // 扫描jar文件 82 | val jarFiles = findJarFiles(File(basePath), indicator) 83 | 84 | if (jarFiles.isEmpty()) { 85 | showInfo(project, "未找到任何jar文件") 86 | return 87 | } 88 | 89 | indicator.text = "正在创建all-in-one目录..." 90 | indicator.fraction = 0.3 91 | 92 | // 创建目标目录 93 | if (!allInOneDir.mkdirs()) { 94 | throw RuntimeException("无法创建all-in-one目录") 95 | } 96 | 97 | indicator.text = "正在复制jar文件..." 98 | indicator.fraction = 0.5 99 | 100 | // 复制文件 101 | copyJarFiles(jarFiles, allInOneDir, indicator) 102 | 103 | indicator.text = "正在添加到项目库..." 104 | indicator.fraction = 0.8 105 | 106 | // 根据版本选择不同的添加方式 107 | if (isNewThreadingModel) { 108 | addDirectoryToLibrary_New(project, allInOneDir) 109 | } else { 110 | addDirectoryToLibrary_Old(project, allInOneDir) 111 | } 112 | 113 | indicator.fraction = 1.0 114 | showSuccess(project, jarFiles.size) 115 | 116 | } catch (e: Exception) { 117 | showError(project, "操作失败:${e.message}") 118 | } 119 | } 120 | }) 121 | } 122 | 123 | /** 124 | * 递归查找所有jar文件 125 | */ 126 | private fun findJarFiles(directory: File, indicator: ProgressIndicator): List { 127 | val jarFiles = mutableListOf() 128 | 129 | fun searchDirectory(dir: File) { 130 | if (indicator.isCanceled) return 131 | 132 | try { 133 | dir.listFiles()?.forEach { file -> 134 | if (indicator.isCanceled) return 135 | 136 | when { 137 | file.isDirectory -> { 138 | // 跳过常见的不需要搜索的目录,提高性能 139 | if (!shouldSkipDirectory(file.name)) { 140 | searchDirectory(file) 141 | } 142 | } 143 | file.isFile && file.name.endsWith(".jar", ignoreCase = true) -> { 144 | jarFiles.add(file) 145 | indicator.text2 = "发现: ${file.name}" 146 | } 147 | } 148 | } 149 | } catch (e: Exception) { 150 | // 忽略无法访问的目录 151 | } 152 | } 153 | 154 | searchDirectory(directory) 155 | return jarFiles 156 | } 157 | 158 | /** 159 | * 判断是否应该跳过某些目录以提高性能 160 | */ 161 | private fun shouldSkipDirectory(dirName: String): Boolean { 162 | return dirName.startsWith(".") || 163 | dirName == "node_modules" || 164 | dirName == "target" || 165 | dirName == "build" || 166 | dirName == ".gradle" || 167 | dirName == ".mvn" 168 | } 169 | 170 | /** 171 | * 复制jar文件,添加重名处理 172 | */ 173 | private fun copyJarFiles(jarFiles: List, targetDir: File, indicator: ProgressIndicator) { 174 | val nameCount = mutableMapOf() 175 | 176 | jarFiles.forEachIndexed { index, jarFile -> 177 | if (indicator.isCanceled) return 178 | 179 | try { 180 | // 处理重名文件 181 | var targetName = jarFile.name 182 | val baseName = jarFile.nameWithoutExtension 183 | val extension = jarFile.extension 184 | 185 | if (nameCount.containsKey(targetName)) { 186 | val count = nameCount[targetName]!! + 1 187 | nameCount[targetName] = count 188 | targetName = "${baseName}_$count.$extension" 189 | } else { 190 | nameCount[targetName] = 1 191 | } 192 | 193 | val targetFile = File(targetDir, targetName) 194 | Files.copy(jarFile.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING) 195 | 196 | val progress = 0.5 + (index + 1).toDouble() / jarFiles.size * 0.3 197 | indicator.fraction = progress 198 | indicator.text2 = "复制: ${jarFile.name} (${index + 1}/${jarFiles.size})" 199 | 200 | } catch (e: Exception) { 201 | throw RuntimeException("复制文件失败: ${jarFile.name} -> ${e.message}") 202 | } 203 | } 204 | } 205 | 206 | /** 207 | * 2025.1+ 版本 (251+) 的库添加方法 208 | * 使用新的线程模型 209 | */ 210 | private fun addDirectoryToLibrary_New(project: Project, allInOneDir: File) { 211 | ApplicationManager.getApplication().invokeLater { 212 | try { 213 | WriteAction.run { 214 | performLibraryOperations(project, allInOneDir) 215 | } 216 | } catch (e: Exception) { 217 | ApplicationManager.getApplication().invokeLater { 218 | Messages.showErrorDialog(project, "添加到项目库失败:${e.message}", "错误") 219 | } 220 | } 221 | } 222 | } 223 | 224 | /** 225 | * 2024.3及以下版本 (243及以下) 的库添加方法 226 | * 使用旧的线程模型 227 | */ 228 | private fun addDirectoryToLibrary_Old(project: Project, allInOneDir: File) { 229 | ApplicationManager.getApplication().invokeAndWait { 230 | ApplicationManager.getApplication().runWriteAction { 231 | try { 232 | performLibraryOperations(project, allInOneDir) 233 | } catch (e: Exception) { 234 | throw RuntimeException("添加到项目库失败:${e.message}") 235 | } 236 | } 237 | } 238 | } 239 | 240 | /** 241 | * 核心库操作逻辑,两个版本共用 242 | */ 243 | private fun performLibraryOperations(project: Project, allInOneDir: File) { 244 | // 刷新文件系统 245 | LocalFileSystem.getInstance().refresh(false) 246 | val allInOneVirtualDir = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(allInOneDir) 247 | ?: throw RuntimeException("无法找到all-in-one目录") 248 | allInOneVirtualDir.refresh(false, true) 249 | 250 | // 获取项目级别库表 251 | val projectLibraryTable = LibraryTablesRegistrar.getInstance().getLibraryTable(project) 252 | 253 | // 安全地删除同名库 254 | val existingLibrary = projectLibraryTable.getLibraryByName("all-in-one") 255 | existingLibrary?.let { lib -> 256 | // 先从所有模块中移除引用 257 | removeLibraryFromAllModules(project, "all-in-one") 258 | // 然后删除库 259 | projectLibraryTable.removeLibrary(lib) 260 | } 261 | 262 | // 创建新库 263 | val library = projectLibraryTable.createLibrary("all-in-one") 264 | val libraryModel = library.modifiableModel 265 | 266 | try { 267 | // 为每个jar文件添加jar root 268 | val jarChildren = allInOneVirtualDir.children 269 | ?.filter { !it.isDirectory && it.extension?.equals("jar", true) == true } 270 | ?: emptyList() 271 | 272 | if (jarChildren.isEmpty()) { 273 | throw RuntimeException("all-in-one目录中没有找到jar文件") 274 | } 275 | 276 | jarChildren.forEach { vf -> 277 | try { 278 | val jarRoot = JarFileSystem.getInstance().refreshAndFindFileByPath("${vf.path}!/") 279 | jarRoot?.let { 280 | libraryModel.addRoot(it, OrderRootType.CLASSES) 281 | } 282 | } catch (e: Exception) { 283 | // 记录但不中断,继续处理其他jar文件 284 | println("警告:无法添加jar文件 ${vf.name}: ${e.message}") 285 | } 286 | } 287 | 288 | libraryModel.commit() 289 | 290 | // 将该项目库加入到所有模块依赖 291 | addLibraryToAllModules(project, library) 292 | 293 | } catch (e: Exception) { 294 | libraryModel.dispose() 295 | throw e 296 | } 297 | } 298 | 299 | /** 300 | * 从所有模块中移除指定名称的库 301 | */ 302 | private fun removeLibraryFromAllModules(project: Project, libraryName: String) { 303 | ModuleManager.getInstance(project).modules.forEach { module -> 304 | val moduleModel = ModuleRootManager.getInstance(module).modifiableModel 305 | try { 306 | val toRemove = moduleModel.orderEntries 307 | .filterIsInstance() 308 | .filter { it.library?.name == libraryName } 309 | 310 | toRemove.forEach { moduleModel.removeOrderEntry(it) } 311 | moduleModel.commit() 312 | } catch (e: Exception) { 313 | moduleModel.dispose() 314 | // 继续执行,不中断整个流程 315 | } 316 | } 317 | } 318 | 319 | /** 320 | * 将库添加到所有模块 321 | */ 322 | private fun addLibraryToAllModules(project: Project, library: Library) { 323 | ModuleManager.getInstance(project).modules.forEach { module -> 324 | val moduleModel = ModuleRootManager.getInstance(module).modifiableModel 325 | try { 326 | // 检查是否已存在 327 | val exists = moduleModel.orderEntries 328 | .filterIsInstance() 329 | .any { it.library?.name == "all-in-one" } 330 | 331 | if (!exists) { 332 | moduleModel.addLibraryEntry(library) 333 | } 334 | moduleModel.commit() 335 | } catch (e: Exception) { 336 | moduleModel.dispose() 337 | throw RuntimeException("无法将库添加到模块 ${module.name}: ${e.message}") 338 | } 339 | } 340 | } 341 | 342 | private fun showInfo(project: Project, message: String) { 343 | ApplicationManager.getApplication().invokeLater { 344 | Messages.showInfoMessage(project, message, "提示") 345 | } 346 | } 347 | 348 | private fun showError(project: Project, message: String) { 349 | ApplicationManager.getApplication().invokeLater { 350 | Messages.showErrorDialog(project, message, "错误") 351 | } 352 | } 353 | 354 | private fun showSuccess(project: Project, count: Int) { 355 | ApplicationManager.getApplication().invokeLater { 356 | val versionInfo = if (isNewThreadingModel) "2025.1+" else "2024.3-" 357 | Messages.showInfoMessage( 358 | project, 359 | "成功处理了 $count 个jar文件\n所有jar包依赖已添加为库\n(兼容模式: $versionInfo)", 360 | "操作完成" 361 | ) 362 | } 363 | } 364 | 365 | override fun update(e: AnActionEvent) { 366 | e.presentation.isEnabledAndVisible = e.project != null 367 | } 368 | } --------------------------------------------------------------------------------