Maven-本地 Maven 仓库离线合并与修复脚本
背景
在部分项目依赖无法从公共仓库(Maven Central)获取,且公司私有仓库因网络隔离或权限问题不可访问的场景下,若同事本地 .m2/repository 中存在所需依赖,直接复制文件往往会导致元数据缺失、Checksum 校验失败或构建报错。为解决此问题,编写了 maven-repo-manager.sh 脚本,用于安全地从同事的本地仓库合并依赖至当前环境,并自动修复元数据。
脚本功能概述
该脚本是一个基于 Bash 的本地仓库管理工具,核心功能包括:
备份 (backup):在执行高风险操作前自动备份当前仓库。
合并 (merge):将指定源目录下的 JAR/POM 文件解析并重新安装到本地仓库,确保元数据完整。
修复 (fix):扫描现有仓库,重新生成缺失的元数据和校验和。
回滚 (rollback):恢复到最近一次备份状态。
干跑模式 (dry-run):模拟执行过程,不修改实际文件。
核心逻辑实现
1. 依赖解析机制
脚本通过遍历源目录中的 .jar 和 .pom 文件,根据 Maven 本地仓库的标准目录结构反推 GAV(Group ID, Artifact ID, Version):
# 示例路径解析逻辑
# Source: /tmp/repo/com/example/my-lib/1.0.0/my-lib-1.0.0.jar
# groupId: com.example
# artifactId: my-lib
# version: 1.0.0
# packaging: jar/pom
利用 sed 和字符串处理提取路径信息,避免依赖复杂的正则表达式,提高兼容性。
2. 安全合并策略
不同于简单的 cp 命令,脚本调用 Maven 自带的 install:install-file 插件进行重灌:
mvn -q install:install-file \
-Dfile="$file" \
-DgroupId="$groupId" \
-DartifactId="$artifactId" \
-Dversion="$version" \
-Dpackaging="$packaging" \
-DlocalRepositoryPath="$REPO"
优势:
自动生成正确的
.metadata文件(包含版本号列表)。计算并写入
.sha1和.md5校验文件。确保本地仓库索引的一致性。
3. 容错与安全设计
前置备份:每次
merge或fix操作前,自动创建带时间戳的备份目录(如.backup/20260407_180000)。空间检查:在备份前检查磁盘剩余空间,防止因空间不足导致系统异常。
TTL 保护:同一小时内不重复备份,减少冗余 IO。
日志记录:所有操作输出到
logs/manager_YYYYMMDD.log,便于排查问题。确认机制:非静默模式下,关键操作需用户输入
yes确认。
使用示例
场景一:从同事机器合并依赖
假设同事的本地仓库位于 /tmp/colleague-m2:
./maven-repo-manager.sh merge -s /tmp/colleague-m2 -y
参数说明:
-s: 指定源仓库路径。-y: 自动确认,跳过交互提示。
场景二:修复损坏的本地仓库
如果本地仓库元数据损坏,可执行:
./maven-repo-manager.sh fix -y
场景三:预演操作(Dry Run)
在不实际修改文件的情况下查看将要执行的操作:
./maven-repo-manager.sh merge -s /tmp/colleague-m2 --dry-run
注意事项
环境依赖:目标机器必须已安装 Maven 且
mvn命令可用。目录规范:源目录必须符合标准 Maven 本地仓库结构(
groupId/artifactId/version/)。SNAPSHOT 版本:脚本主要针对 Release 版本,SNAPSHOT 版本的动态更新可能需要额外处理。
并发限制:当前版本为串行处理,若依赖数量巨大,建议分批次执行。
总结
该脚本提供了一种轻量级、可控的离线依赖解决方案,特别适用于内网隔离、网络受限或临时应急场景。通过自动化元数据重建和备份回滚机制,有效降低了手动操作带来的风险。后续可根据团队需求扩展对 war/ear 包的支持或增加并行处理优化。
附录
#!/usr/bin/env bash
#
# ------------------------------------------------------------
# Script: maven-repo-manager.sh
# Author: DevOps Expert
# Description:
# Enterprise-grade Maven local repository manager.
# Supports backup, merge, fix, rollback with safety-first design.
#
# Usage:
# ./maven-repo-manager.sh <command> [options]
#
# Commands:
# backup Backup repository
# merge Merge external repo into local repo
# fix Fix metadata and checksums
# rollback Rollback to last backup
# help Show help
#
# Options:
# -r, --repo <path> Local repo path (default ~/.m2/repository)
# -s, --source <path> Source repo path (for merge)
# -f, --force Force overwrite
# -y, --yes Auto confirm
# -j, --jobs <num> Parallel jobs (default 4)
# --dry-run Simulation mode
#
# Example:
# ./maven-repo-manager.sh merge -s /tmp/repo -y
#
# ------------------------------------------------------------
set -euo pipefail
#######################################
# CONFIG
#######################################
DEFAULT_REPO="$HOME/.m2/repository"
BACKUP_DIR=".backup"
LOG_DIR="logs"
BACKUP_TTL=3600
JOBS=4
DRY_RUN=false
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'
#######################################
# GLOBAL VARS
#######################################
REPO="$DEFAULT_REPO"
SOURCE=""
FORCE=false
YES=false
START_TIME=$(date +%s)
LOG_FILE=""
GROUP_INPUT=""
#######################################
# LOGGING
#######################################
init_log() {
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/manager_$(date +%Y%m%d).log"
}
log() {
local level="$1"; shift
local msg="$*"
echo -e "$msg"
echo "[$(date '+%F %T')] [$level] $msg" >> "$LOG_FILE"
}
log_info() { log "INFO" "${GREEN}[INFO]${NC} $*"; }
log_warn() { log "WARN" "${YELLOW}[WARN]${NC} $*"; }
log_error() { log "ERROR" "${RED}[ERROR]${NC} $*"; }
#######################################
# UTIL
#######################################
confirm() {
if [ "$YES" = true ]; then return 0; fi
echo -n "Type 'yes' to continue: "
read -r ans
[ "$ans" = "yes" ]
}
run_cmd() {
if [ "$DRY_RUN" = true ]; then
log_info "[DRY-RUN] $*"
else
eval "$@"
fi
}
get_repo_size() {
du -sk "$REPO" | awk '{print $1}'
}
get_free_space() {
df -k "$REPO" | awk 'NR==2 {print $4}'
}
#######################################
# BACKUP
#######################################
backup_repo() {
mkdir -p "$BACKUP_DIR"
local last_backup
last_backup=$(ls -1 "$BACKUP_DIR" 2>/dev/null | tail -n1 || true)
if [ -n "$last_backup" ]; then
local last_time
last_time=$(date -j -f "%Y%m%d_%H%M%S" "$last_backup" +%s 2>/dev/null || date -d "$last_backup" +%s)
local now
now=$(date +%s)
if (( now - last_time < BACKUP_TTL )); then
log_warn "Skip backup (within TTL)"
return
fi
fi
local repo_size free_space
repo_size=$(get_repo_size)
free_space=$(get_free_space)
if (( free_space < repo_size * 12 / 10 )); then
log_error "Not enough disk space"
exit 1
fi
local ts
ts=$(date +%Y%m%d_%H%M%S)
local target="$BACKUP_DIR/$ts"
log_info "Creating backup: $target"
run_cmd "cp -a \"$REPO\" \"$target\""
}
#######################################
# ROLLBACK
#######################################
rollback_repo() {
local last
last=$(ls -1 "$BACKUP_DIR" | tail -n1)
if [ -z "$last" ]; then
log_error "No backup found"
exit 1
fi
log_warn "Rollback to $last"
confirm || exit 1
run_cmd "rm -rf \"$REPO\""
run_cmd "cp -a \"$BACKUP_DIR/$last\" \"$REPO\""
log_info "Rollback complete"
}
#######################################
# GAV PARSE
#######################################
parse_pom() {
local pom="$1"
groupId=$(sed -n 's:.*<groupId>\(.*\)</groupId>.*:\1:p' "$pom" | head -1)
artifactId=$(sed -n 's:.*<artifactId>\(.*\)</artifactId>.*:\1:p' "$pom" | head -1)
version=$(sed -n 's:.*<version>\(.*\)</version>.*:\1:p' "$pom" | head -1)
packaging=$(sed -n 's:.*<packaging>\(.*\)</packaging>.*:\1:p' "$pom" | head -1)
packaging=${packaging:-jar}
}
#######################################
# FIX MISSING (NEW FUNCTION)
#######################################
fix_missing() {
if [ -z "$GROUP_INPUT" ]; then
log_error "Missing required argument: -g <groupId:artifactId:version>"
log_info "Example: ./maven-repo-manager.sh fix-missing -g com.taobao:parent:1.0.1"
exit 1
fi
log_info "Parsing GAV input: $GROUP_INPUT"
# 智能解析 groupId:artifactId:version
IFS=':' read -r gid aid ver <<< "$GROUP_INPUT"
if [ -z "$gid" ] || [ -z "$aid" ] || [ -z "$ver" ]; then
log_error "Invalid format. Expected 'groupId:artifactId:version'"
exit 1
fi
log_info "Resolved: Group=$gid, Artifact=$aid, Version=$ver"
# 创建临时目录
local TEMP_DIR
TEMP_DIR=$(mktemp -d)
local POM_FILE="$TEMP_DIR/pom.xml"
# 生成最小化合法 POM
cat > "$POM_FILE" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>$gid</groupId>
<artifactId>$aid</artifactId>
<version>$ver</version>
<packaging>pom</packaging>
<name>Placeholder Parent POM</name>
<description>Minimal placeholder generated by maven-repo-manager to resolve missing dependency.</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencyManagement/>
<build/>
</project>
EOF
# 执行安装
log_info "Installing placeholder to local repo..."
# 构建命令
local CMD="mvn -q install:install-file \
-Dfile=\"$POM_FILE\" \
-DgroupId=\"$gid\" \
-DartifactId=\"$aid\" \
-Dversion=\"$ver\" \
-Dpackaging=pom \
-DlocalRepositoryPath=\"$REPO\""
if [ "$DRY_RUN" = true ]; then
log_info "[DRY-RUN] Would execute: $CMD"
else
eval "$CMD"
if [ $? -ne 0 ]; then
log_error "Failed to install placeholder."
rm -rf "$TEMP_DIR"
exit 1
fi
fi
# 清理
rm -rf "$TEMP_DIR"
log_info "✅ Placeholder created successfully: $gid:$aid:$ver"
}
#######################################
# MERGE
#######################################
merge_repo() {
[ -z "$SOURCE" ] && { log_error "Source required"; exit 1; }
backup_repo
log_info "Merging via Maven install..."
find "$SOURCE" -type f \( -name "*.jar" -o -name "*.pom" \) | while read -r file; do
dir=$(dirname "$file")
version=$(basename "$dir")
artifact_dir=$(dirname "$dir")
artifactId=$(basename "$artifact_dir")
groupPath=$(echo "$artifact_dir" | sed "s|$SOURCE/||")
groupId=$(echo "$groupPath" | sed "s|/$artifactId||" | tr '/' '.')
filename=$(basename "$file")
# 推断 packaging
case "$filename" in
*.pom)
packaging="pom"
;;
*.jar)
packaging="jar"
;;
*)
log_warn "Skip unsupported file: $file"
continue
;;
esac
log_info "Installing: $groupId:$artifactId:$version ($packaging)"
cmd="mvn -q install:install-file \
-Dfile=\"$file\" \
-DgroupId=\"$groupId\" \
-DartifactId=\"$artifactId\" \
-Dversion=\"$version\" \
-Dpackaging=\"$packaging\" \
-DlocalRepositoryPath=\"$REPO\""
if [ "$FORCE" = true ]; then
cmd="$cmd -DupdateReleaseInfo=true"
fi
run_cmd "$cmd"
done
log_info "Merge (via mvn) complete"
}
#######################################
# CHECKSUM
#######################################
fix_repo() {
backup_repo
log_info "Fixing repo via Maven reinstall..."
find "$REPO" -type f \( -name "*.jar" -o -name "*.pom" \) | while read -r file; do
dir=$(dirname "$file")
version=$(basename "$dir")
artifact_dir=$(dirname "$dir")
artifactId=$(basename "$artifact_dir")
groupPath=$(echo "$artifact_dir" | sed "s|$REPO/||")
groupId=$(echo "$groupPath" | sed "s|/$artifactId||" | tr '/' '.')
filename=$(basename "$file")
case "$filename" in
*.pom)
packaging="pom"
;;
*.jar)
packaging="jar"
;;
*)
continue
;;
esac
log_info "Reinstalling: $groupId:$artifactId:$version"
cmd="mvn -q install:install-file \
-Dfile=\"$file\" \
-DgroupId=\"$groupId\" \
-DartifactId=\"$artifactId\" \
-Dversion=\"$version\" \
-Dpackaging=\"$packaging\" \
-DlocalRepositoryPath=\"$REPO\""
run_cmd "$cmd"
done
log_info "Fix complete (Maven rebuild)"
}
#######################################
# ARG PARSE
#######################################
parse_args() {
COMMAND="${1:-help}"
shift || true
while [[ $# -gt 0 ]]; do
case "$1" in
-r|--repo) REPO="$2"; shift 2 ;;
-s|--source) SOURCE="$2"; shift 2 ;;
-f|--force) FORCE=true; shift ;;
-y|--yes) YES=true; shift ;;
-j|--jobs) JOBS="$2"; shift 2 ;;
--dry-run) DRY_RUN=true; shift ;;
-g|--group) GROUP_INPUT="$2"; shift 2 ;;
*) shift ;;
esac
done
}
#######################################
# HELP
#######################################
show_help() {
echo "Maven Repo Manager"
echo ""
echo "Usage: $0 <command> [options]"
echo ""
echo "Commands:"
echo " backup Backup repository"
echo " merge -s <path> Merge external repo into local repo"
echo " fix Fix metadata and checksums"
echo " rollback Rollback to last backup"
echo " fix-missing -g <gav> Create placeholder for missing dependency (e.g., com.taobao:parent:1.0.1)"
echo ""
echo "Options:"
echo " -r, --repo <path> Local repo path (default ~/.m2/repository)"
echo " -s, --source <path> Source repo path (for merge)"
echo " -g, --group <gav> GAV string for fix-missing command"
echo " -f, --force Force overwrite"
echo " -y, --yes Auto confirm"
echo " -j, --jobs <num> Parallel jobs (default 4)"
echo " --dry-run Simulation mode"
echo ""
echo "Examples:"
echo " $0 merge -s /tmp/repo -y"
echo " $0 fix-missing -g com.kingbase8:pgjdbc-core-parent:1.1.2"
}
#######################################
# MAIN
#######################################
main() {
parse_args "$@"
init_log
case "$COMMAND" in
backup) backup_repo ;;
merge) merge_repo ;;
fix) fix_repo ;;
rollback) rollback_repo ;;
fix-missing) fix_missing ;;
help|*) show_help ;;
esac
local end
end=$(date +%s)
log_info "Done in $((end - START_TIME))s"
}
main "$@"