mirror of
https://github.com/Molunerfinn/PicGo.git
synced 2026-09-20 03:16:37 +00:00
📦 Chore(multi-arch): add multi-arch build support (#1379)
This commit is contained in:
+213
-83
@@ -6,11 +6,6 @@ on:
|
||||
- v*
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test_upload_dist:
|
||||
description: "Test upload-dist.js script"
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
build_os:
|
||||
description: "Build for specific OS: Windows, macOS, Linux, All"
|
||||
required: true
|
||||
@@ -21,97 +16,243 @@ on:
|
||||
- macOS
|
||||
- Linux
|
||||
- All
|
||||
test_upload_dist:
|
||||
description: "Test upload-dist.js script"
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
test_upload_dist_to_dev:
|
||||
description: "Test upload-dist.js script to dev folder"
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
skip_notarize:
|
||||
description: "Skip Notarization (true/false)"
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.x
|
||||
|
||||
jobs:
|
||||
# parallel build jobs
|
||||
build:
|
||||
name: Build on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
# ============== macOS Builds ==============
|
||||
build-macos:
|
||||
name: Build macOS (${{ matrix.arch }})
|
||||
if: github.event.inputs.build_os == 'macOS' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
# if one job fails, do not stop other jobs
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-latest, windows-latest, ubuntu-latest]
|
||||
include:
|
||||
- runner: macos-15-intel
|
||||
target: dmg
|
||||
arch: x64
|
||||
- runner: macos-latest
|
||||
target: dmg
|
||||
arch: arm64
|
||||
steps:
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
- name: Clean workspace on Windows
|
||||
if: runner.os == 'Windows' && (github.event.inputs.build_os == 'Windows' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v'))
|
||||
|
||||
- name: Clean workspace
|
||||
run: rm -rf dist dist_electron node_modules ~/.cache/electron-builder ~/.cache/electron
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build macOS App (${{ matrix.arch }})
|
||||
run: pnpm run build && pnpm exec electron-builder --config electron-builder.config.ts --mac ${{ matrix.target }} --${{ matrix.arch }} --publish never
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
SKIP_NOTARIZE: ${{ inputs.skip_notarize }}
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: PicGo-macOS-${{ matrix.arch }}
|
||||
path: dist/*.*
|
||||
|
||||
# ============== Windows Builds ==============
|
||||
build-windows:
|
||||
name: Build Windows (${{ matrix.arch }})
|
||||
if: github.event.inputs.build_os == 'Windows' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: windows-latest
|
||||
arch: x64-ia32
|
||||
build_arch: --x64 --ia32
|
||||
target: nsis
|
||||
- runner: windows-11-arm
|
||||
arch: arm64
|
||||
build_arch: --arm64
|
||||
target: nsis
|
||||
steps:
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
- name: Clean workspace
|
||||
run: |
|
||||
if (Test-Path dist) { Remove-Item -Recurse -Force dist }
|
||||
if (Test-Path dist_electron) { Remove-Item -Recurse -Force dist_electron }
|
||||
if (Test-Path node_modules) { Remove-Item -Recurse -Force node_modules }
|
||||
|
||||
if (Test-Path "$env:LOCALAPPDATA\electron-builder") {
|
||||
Remove-Item "$env:LOCALAPPDATA\electron-builder" -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
if (Test-Path "$env:LOCALAPPDATA\electron") {
|
||||
Remove-Item "$env:LOCALAPPDATA\electron" -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
- name: Clean workspace on macOS & Linux
|
||||
if: runner.os == 'macOS' || runner.os == 'Linux' && (github.event.inputs.build_os == 'macOS' || github.event.inputs.build_os == 'Linux' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v'))
|
||||
run: |
|
||||
rm -rf dist dist_electron node_modules ~/.cache/electron-builder ~/.cache/electron
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Ubuntu Update with sudo
|
||||
if: runner.os == 'Linux' && (github.event.inputs.build_os == 'Linux' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v'))
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libfuse2
|
||||
- name: Build Windows x64 & ARM64 App
|
||||
if: runner.os == 'Windows' && (github.event.inputs.build_os == 'Windows' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v'))
|
||||
run: pnpm run build:win || true
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
- name: Build macOS x64 & ARM64 App
|
||||
if: runner.os == 'macOS' && (github.event.inputs.build_os == 'macOS' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v'))
|
||||
run: pnpm run build:mac || true
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
# macOS Code Signing
|
||||
# p12 证书的 Base64 字符串
|
||||
CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
|
||||
# p12 证书密码
|
||||
CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
|
||||
# macOS Notarization (公证所需变量)
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
SKIP_NOTARIZE: ${{ inputs.skip_notarize }}
|
||||
- name: Build Linux x64 & ARM64 App
|
||||
if: runner.os == 'Linux' && (github.event.inputs.build_os == 'Linux' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v'))
|
||||
run: pnpm run build:linux || true
|
||||
shell: bash
|
||||
|
||||
- name: Build Windows App (${{ matrix.arch }})
|
||||
run: pnpm run build && pnpm exec electron-builder --config electron-builder.config.ts --win ${{matrix.target}} ${{ matrix.build_arch }} --publish never
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: PicGo-${{ runner.os }}
|
||||
name: PicGo-Windows-${{ matrix.arch }}
|
||||
path: dist/*.*
|
||||
- name: Upload to release.picgo.app
|
||||
if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.test_upload_dist
|
||||
|
||||
# ============== Linux Builds ==============
|
||||
build-linux:
|
||||
name: Build Linux (${{ matrix.arch }})
|
||||
if: github.event.inputs.build_os == 'Linux' || github.event.inputs.build_os == 'All' || startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-latest
|
||||
arch: x64
|
||||
target: AppImage deb snap
|
||||
- runner: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
target: AppImage deb
|
||||
steps:
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
- name: Clean workspace
|
||||
run: rm -rf dist dist_electron node_modules ~/.cache/electron-builder ~/.cache/electron
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Install Linux dependencies
|
||||
run: |
|
||||
pnpm run upload-dist
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libfuse2
|
||||
|
||||
- name: Build Linux App (${{ matrix.arch }})
|
||||
run: pnpm run build && pnpm exec electron-builder --config electron-builder.config.ts --linux ${{matrix.target}} --${{ matrix.arch }} --publish never
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: PicGo-Linux-${{ matrix.arch }}
|
||||
path: dist/*.*
|
||||
|
||||
# ============== Release ==============
|
||||
release:
|
||||
name: Merge & Release
|
||||
needs: [build-macos, build-windows, build-linux]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
cache-dependency-path: pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: List artifacts
|
||||
run: ls -laR artifacts/
|
||||
|
||||
- name: Merge artifacts and yml files
|
||||
run: node scripts/merge-artifacts.js
|
||||
|
||||
- name: List dist
|
||||
run: ls -la dist/
|
||||
|
||||
- name: Upload to release.picgo.app
|
||||
if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.test_upload_dist || github.event.inputs.test_upload_dist_to_dev
|
||||
run: |
|
||||
ARGS="--all"
|
||||
|
||||
if [[ "${{ github.event.inputs.test_upload_dist_to_dev }}" == "true" ]]; then
|
||||
ARGS="$ARGS --dev"
|
||||
echo "🚧 Test Upload Mode: ON"
|
||||
fi
|
||||
|
||||
node scripts/upload-dist.js $ARGS
|
||||
env:
|
||||
PICGO_ENV_S3_SECRET_ID: ${{ secrets.PICGO_ENV_S3_SECRET_ID }}
|
||||
PICGO_ENV_S3_SECRET_KEY: ${{ secrets.PICGO_ENV_S3_SECRET_KEY }}
|
||||
@@ -119,17 +260,7 @@ jobs:
|
||||
PICGO_ENV_S3_LEGACY_ACCOUNT_ID: ${{ secrets.PICGO_ENV_S3_LEGACY_ACCOUNT_ID }}
|
||||
PICGO_ENV_S3_LEGACY_SECRET_ID: ${{ secrets.PICGO_ENV_S3_LEGACY_SECRET_ID }}
|
||||
PICGO_ENV_S3_LEGACY_SECRET_KEY: ${{ secrets.PICGO_ENV_S3_LEGACY_SECRET_KEY }}
|
||||
release:
|
||||
name: Publish GitHub Release
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Publish GitHub Dev Release
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: softprops/action-gh-release@v2
|
||||
@@ -140,16 +271,16 @@ jobs:
|
||||
draft: true
|
||||
prerelease: false
|
||||
files: |
|
||||
!artifacts/**/*-unpacked/**
|
||||
artifacts/**/*.exe
|
||||
artifacts/**/*.dmg
|
||||
artifacts/**/*.zip
|
||||
artifacts/**/*.AppImage
|
||||
artifacts/**/*.deb
|
||||
artifacts/**/*.snap
|
||||
artifacts/**/*.tar.gz
|
||||
artifacts/**/*.yml
|
||||
artifacts/**/*.blockmap
|
||||
dist/*.exe
|
||||
dist/*.dmg
|
||||
dist/*.zip
|
||||
dist/*.AppImage
|
||||
dist/*.deb
|
||||
dist/*.snap
|
||||
dist/*.tar.gz
|
||||
dist/*.yml
|
||||
dist/*.blockmap
|
||||
|
||||
- name: Publish GitHub Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: softprops/action-gh-release@v2
|
||||
@@ -160,13 +291,12 @@ jobs:
|
||||
draft: true
|
||||
prerelease: false
|
||||
files: |
|
||||
!artifacts/**/*-unpacked/**
|
||||
artifacts/**/*.exe
|
||||
artifacts/**/*.dmg
|
||||
artifacts/**/*.zip
|
||||
artifacts/**/*.AppImage
|
||||
artifacts/**/*.deb
|
||||
artifacts/**/*.snap
|
||||
artifacts/**/*.tar.gz
|
||||
artifacts/**/*.yml
|
||||
artifacts/**/*.blockmap
|
||||
dist/*.exe
|
||||
dist/*.dmg
|
||||
dist/*.zip
|
||||
dist/*.AppImage
|
||||
dist/*.deb
|
||||
dist/*.snap
|
||||
dist/*.tar.gz
|
||||
dist/*.yml
|
||||
dist/*.blockmap
|
||||
|
||||
@@ -3,7 +3,7 @@ import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const shouldNotarize = process.env.SKIP_NOTARIZE !== 'true';
|
||||
const shouldNotarize = process.env.SKIP_NOTARIZE !== 'true'
|
||||
|
||||
const config: Configuration = {
|
||||
appId: 'com.molunerfinn.picgo',
|
||||
|
||||
+3
-3
@@ -9,7 +9,7 @@
|
||||
"email": "marksz@teamsz.xyz"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "electron-vite build && electron-builder --config electron-builder.config.ts --publish never",
|
||||
"build": "electron-vite build",
|
||||
"build:win": "npm run build && electron-builder --config electron-builder.config.ts --win --publish never",
|
||||
"build:mac": "npm run build && electron-builder --config electron-builder.config.ts --mac --publish never",
|
||||
"build:linux": "npm run build && electron-builder --config electron-builder.config.ts --linux --publish never",
|
||||
@@ -44,7 +44,7 @@
|
||||
"element-plus": "^2.3.7",
|
||||
"epipebomb": "^1.0.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"keycode": "^2.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-id": "^0.14.0",
|
||||
@@ -132,4 +132,4 @@
|
||||
"commit-msg": "npm run lint:dpdm && commitlint -E HUSKY_GIT_PARAMS"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1
-1
@@ -45,7 +45,7 @@ importers:
|
||||
specifier: ^10.0.0
|
||||
version: 10.1.0
|
||||
js-yaml:
|
||||
specifier: ^4.1.0
|
||||
specifier: ^4.1.1
|
||||
version: 4.1.1
|
||||
keycode:
|
||||
specifier: ^2.2.0
|
||||
|
||||
+3
-3
@@ -16,15 +16,15 @@ const macos = [
|
||||
|
||||
const linux = {
|
||||
AppImage: [
|
||||
{ label: '64 bit', arch: '-x64', ext: '.AppImage' },
|
||||
{ label: '64 bit', arch: '-amd64', ext: '.AppImage' },
|
||||
{ label: 'ARM64', arch: '-arm64', ext: '.AppImage' }
|
||||
],
|
||||
Deb: [
|
||||
{ label: '64 bit', arch: '-x64', ext: '.deb' },
|
||||
{ label: '64 bit', arch: '-amd64', ext: '.deb' },
|
||||
{ label: 'ARM64', arch: '-arm64', ext: '.deb' }
|
||||
],
|
||||
Snap: [
|
||||
{ label: '64 bit', arch: '-x64', ext: '.snap' }
|
||||
{ label: '64 bit', arch: '-amd64', ext: '.snap' }
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Merge artifacts from different platforms and architectures
|
||||
* Also merge latest*.yml files for electron-updater
|
||||
*/
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const yaml = require('js-yaml')
|
||||
|
||||
const ARTIFACTS_DIR = path.join(__dirname, '../artifacts')
|
||||
const DIST_DIR = path.join(__dirname, '../dist')
|
||||
|
||||
// yml 文件分组规则
|
||||
const YML_MERGE_RULES = {
|
||||
// macOS: 合并 x64 和 arm64 的 latest-mac.yml
|
||||
'latest-mac.yml': ['latest-mac.yml'],
|
||||
// Windows: 合并所有架构的 latest.yml
|
||||
'latest.yml': ['latest.yml'],
|
||||
// Linux x64: latest-linux.yml
|
||||
'latest-linux.yml': ['latest-linux.yml'],
|
||||
// Linux arm64: latest-linux-arm64.yml
|
||||
'latest-linux-arm64.yml': ['latest-linux-arm64.yml']
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归查找指定文件名的所有文件
|
||||
*/
|
||||
function findFiles(dir, filename) {
|
||||
const results = []
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
return results
|
||||
}
|
||||
|
||||
const items = fs.readdirSync(dir)
|
||||
|
||||
for (const item of items) {
|
||||
const fullPath = path.join(dir, item)
|
||||
const stat = fs.statSync(fullPath)
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
results.push(...findFiles(fullPath, filename))
|
||||
} else if (item === filename) {
|
||||
results.push(fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并多个 yml 文件
|
||||
*/
|
||||
function mergeYmlFiles(files) {
|
||||
if (files.length === 0) return null
|
||||
if (files.length === 1) {
|
||||
return yaml.load(fs.readFileSync(files[0], 'utf8'))
|
||||
}
|
||||
|
||||
const contents = files.map(f => yaml.load(fs.readFileSync(f, 'utf8')))
|
||||
|
||||
// 以第一个为基准,合并 files 数组
|
||||
const merged = {
|
||||
version: contents[0].version,
|
||||
files: [],
|
||||
releaseDate: contents[0].releaseDate
|
||||
}
|
||||
|
||||
for (const content of contents) {
|
||||
if (content.files && Array.isArray(content.files)) {
|
||||
merged.files.push(...content.files)
|
||||
}
|
||||
}
|
||||
|
||||
// 去重(根据 sha512)
|
||||
const seen = new Set()
|
||||
merged.files = merged.files.filter(file => {
|
||||
const key = file.sha512
|
||||
if (seen.has(key)) return false
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
|
||||
// 设置 path/sha512/size 为第一个文件(electron-updater 兼容性)
|
||||
if (merged.files.length > 0) {
|
||||
merged.path = merged.files[0].url
|
||||
merged.sha512 = merged.files[0].sha512
|
||||
merged.size = merged.files[0].size
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制所有构建产物到 dist 目录
|
||||
*/
|
||||
function copyArtifacts() {
|
||||
console.log('📁 Copying all artifacts to dist...\n')
|
||||
|
||||
if (!fs.existsSync(ARTIFACTS_DIR)) {
|
||||
console.log('⚠️ No artifacts directory found')
|
||||
return
|
||||
}
|
||||
|
||||
const platformDirs = fs.readdirSync(ARTIFACTS_DIR)
|
||||
|
||||
for (const platformDir of platformDirs) {
|
||||
const platformPath = path.join(ARTIFACTS_DIR, platformDir)
|
||||
const stat = fs.statSync(platformPath)
|
||||
|
||||
if (!stat.isDirectory()) continue
|
||||
|
||||
console.log(`📦 Processing ${platformDir}...`)
|
||||
const files = fs.readdirSync(platformPath)
|
||||
|
||||
for (const file of files) {
|
||||
const srcPath = path.join(platformPath, file)
|
||||
const destPath = path.join(DIST_DIR, file)
|
||||
const fileStat = fs.statSync(srcPath)
|
||||
|
||||
// 跳过目录和 yml 文件(yml 文件会单独处理合并)
|
||||
if (fileStat.isDirectory()) continue
|
||||
if (file.endsWith('.yml')) continue
|
||||
|
||||
// 如果目标文件已存在且大小相同,跳过
|
||||
if (fs.existsSync(destPath)) {
|
||||
const destStat = fs.statSync(destPath)
|
||||
if (destStat.size === fileStat.size) {
|
||||
console.log(` ⏭️ Skipped (exists): ${file}`)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
fs.copyFileSync(srcPath, destPath)
|
||||
console.log(` ✅ Copied: ${file}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并 yml 文件
|
||||
*/
|
||||
function mergeYmlFilesFromArtifacts() {
|
||||
console.log('\n🔀 Merging yml files...\n')
|
||||
|
||||
for (const [outputName, sourceNames] of Object.entries(YML_MERGE_RULES)) {
|
||||
const allFiles = []
|
||||
|
||||
for (const sourceName of sourceNames) {
|
||||
const files = findFiles(ARTIFACTS_DIR, sourceName)
|
||||
allFiles.push(...files)
|
||||
}
|
||||
|
||||
if (allFiles.length === 0) {
|
||||
console.log(`⏭️ No ${outputName} found, skipping...`)
|
||||
continue
|
||||
}
|
||||
|
||||
console.log(`📄 Found ${allFiles.length} ${outputName} file(s):`)
|
||||
allFiles.forEach(f => console.log(` - ${path.relative(ARTIFACTS_DIR, f)}`))
|
||||
|
||||
const merged = mergeYmlFiles(allFiles)
|
||||
|
||||
if (merged) {
|
||||
const outputPath = path.join(DIST_DIR, outputName)
|
||||
fs.writeFileSync(outputPath, yaml.dump(merged, { lineWidth: -1 }))
|
||||
console.log(`✅ Merged -> ${outputName}`)
|
||||
|
||||
if (merged.files) {
|
||||
console.log(` Files: ${merged.files.map(f => f.url).join(', ')}`)
|
||||
}
|
||||
console.log('')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 Starting artifact merge process...\n')
|
||||
|
||||
// 确保 dist 目录存在
|
||||
if (!fs.existsSync(DIST_DIR)) {
|
||||
fs.mkdirSync(DIST_DIR, { recursive: true })
|
||||
}
|
||||
|
||||
// 1. 复制所有构建产物
|
||||
copyArtifacts()
|
||||
|
||||
// 2. 合并 yml 文件
|
||||
mergeYmlFilesFromArtifacts()
|
||||
|
||||
console.log('🎉 Artifact merge completed!')
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('❌ Error:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
+118
-147
@@ -2,8 +2,6 @@
|
||||
// upload version file to cos
|
||||
|
||||
require('dotenv').config()
|
||||
// const crypto = require('crypto')
|
||||
// const axios = require('axios').default
|
||||
const fs = require('fs')
|
||||
const pkg = require('../package.json')
|
||||
const configList = require('./config')
|
||||
@@ -12,15 +10,13 @@ const path = require('path')
|
||||
const distPath = path.join(__dirname, '../dist')
|
||||
const S3Client = require('@aws-sdk/client-s3').S3Client
|
||||
const Upload = require('@aws-sdk/lib-storage').Upload
|
||||
// const BUCKET = 'picgo-1251750343'
|
||||
// const COS_SECRET_ID = process.env.PICGO_ENV_COS_SECRET_ID
|
||||
// const COS_SECRET_KEY = process.env.PICGO_ENV_COS_SECRET_KEY
|
||||
const uploadToDev = process.argv.includes('--dev')
|
||||
|
||||
const S3_BUCKET = 'release'
|
||||
const S3_LEGACY_BUCKET = 'picgo'
|
||||
// const AREA = 'ap-chengdu'
|
||||
const VERSION = pkg.version
|
||||
const FILE_PATH = `${VERSION}/`
|
||||
const DEV_DIST_PREFIX = 'dev/'
|
||||
const FILE_PATH = uploadToDev ? `${DEV_DIST_PREFIX}${VERSION}/` : `${VERSION}/`
|
||||
const S3_SECRET_ID = process.env.PICGO_ENV_S3_SECRET_ID
|
||||
const S3_SECRET_KEY = process.env.PICGO_ENV_S3_SECRET_KEY
|
||||
const S3_ACCOUNT_ID = process.env.PICGO_ENV_S3_ACCOUNT_ID
|
||||
@@ -49,155 +45,130 @@ const S3LegacyOptions = {
|
||||
region: 'auto'
|
||||
}
|
||||
|
||||
// https://cloud.tencent.com/document/product/436/7778#signature
|
||||
// /**
|
||||
// * @param {string} fileName
|
||||
// * @returns
|
||||
// */
|
||||
// const generateSignature = (fileName, folder = FILE_PATH) => {
|
||||
// const secretKey = COS_SECRET_ID
|
||||
// // const area = AREA
|
||||
// const bucket = BUCKET
|
||||
// const path = folder
|
||||
// const today = Math.floor(new Date().getTime() / 1000)
|
||||
// const tomorrow = today + 86400
|
||||
// const signTime = `${today};${tomorrow}`
|
||||
// const signKey = crypto.createHmac('sha1', secretKey).update(signTime).digest('hex')
|
||||
// const httpString = `put\n/${path}${fileName}\n\nhost=${bucket}.cos.accelerate.myqcloud.com\n`
|
||||
// const sha1edHttpString = crypto.createHash('sha1').update(httpString).digest('hex')
|
||||
// const stringToSign = `sha1\n${signTime}\n${sha1edHttpString}\n`
|
||||
// const signature = crypto.createHmac('sha1', signKey).update(stringToSign).digest('hex')
|
||||
// return {
|
||||
// signature,
|
||||
// signTime
|
||||
// }
|
||||
// }
|
||||
|
||||
// /**
|
||||
// *
|
||||
// * @param {string} fileName
|
||||
// * @param {Buffer} fileBuffer
|
||||
// * @param {{ signature: string, signTime: string }} signature
|
||||
// * @returns
|
||||
// */
|
||||
// const getReqOptions = (fileName, fileBuffer, signature, folder = FILE_PATH) => {
|
||||
// return {
|
||||
// method: 'PUT',
|
||||
// url: `http://${BUCKET}.cos.accelerate.myqcloud.com/${encodeURI(folder)}${encodeURI(fileName)}`,
|
||||
// headers: {
|
||||
// Host: `${BUCKET}.cos.accelerate.myqcloud.com`,
|
||||
// Authorization: `q-sign-algorithm=sha1&q-ak=${COS_SECRET_KEY}&q-sign-time=${signature.signTime}&q-key-time=${signature.signTime}&q-header-list=host&q-url-param-list=&q-signature=${signature.signature}`,
|
||||
// contentType: mime.lookup(fileName),
|
||||
// useAgent: `PicGo;${pkg.version};null;null`
|
||||
// },
|
||||
// maxContentLength: Infinity,
|
||||
// maxBodyLength: Infinity,
|
||||
// data: fileBuffer,
|
||||
// resolveWithFullResponse: true
|
||||
// }
|
||||
// }
|
||||
/**
|
||||
* 检查是否使用 --all 参数(上传所有平台)
|
||||
*/
|
||||
function shouldUploadAll() {
|
||||
return process.argv.includes('--all')
|
||||
}
|
||||
|
||||
/**
|
||||
* a backup for version file
|
||||
* 获取要上传的配置列表
|
||||
*/
|
||||
// const uploadVersionFile = async () => {
|
||||
// try {
|
||||
// const platform = process.platform
|
||||
// if (configList[platform]) {
|
||||
// let versionFileHasUploaded = false
|
||||
// for (const [, config] of configList[platform].entries()) {
|
||||
// const versionFilePath = path.join(distPath, config['version-file'])
|
||||
// let versionFileName = config['version-file']
|
||||
// if (VERSION.toLocaleLowerCase().includes('beta')) {
|
||||
// versionFileName = versionFileName.replace('.yml', '.beta.yml')
|
||||
// }
|
||||
// // upload version file
|
||||
// if (!versionFileHasUploaded) {
|
||||
// const signature = generateSignature(versionFileName, '')
|
||||
// const reqOptions = getReqOptions(versionFileName, fs.readFileSync(versionFilePath), signature, '')
|
||||
// console.log('[PicGo Version File] Uploading...', versionFileName)
|
||||
// await axios.request(reqOptions)
|
||||
// versionFileHasUploaded = true
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// console.warn('platform not supported!', platform)
|
||||
// }
|
||||
// } catch (e) {
|
||||
// console.error(e)
|
||||
// }
|
||||
// }
|
||||
function getUploadConfigs() {
|
||||
if (shouldUploadAll()) {
|
||||
// 合并所有平台的配置
|
||||
return [
|
||||
...configList.darwin,
|
||||
...configList.win32,
|
||||
...configList.linux
|
||||
]
|
||||
}
|
||||
// 原有逻辑:根据当前平台
|
||||
const platform = process.platform
|
||||
return configList[platform] || []
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传单个文件到 S3
|
||||
*/
|
||||
async function uploadFileToS3(client, bucket, key, filePath, contentType = 'application/octet-stream') {
|
||||
const upload = new Upload({
|
||||
client,
|
||||
params: {
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
Body: fs.createReadStream(filePath),
|
||||
ContentType: contentType
|
||||
}
|
||||
})
|
||||
|
||||
upload.on('httpUploadProgress', progress => {
|
||||
const percent = progress.total ? Math.round((progress.loaded / progress.total) * 100) : 0
|
||||
process.stdout.write(`\r Progress: ${progress.loaded}/${progress.total || '?'} (${percent}%)`)
|
||||
})
|
||||
|
||||
await upload.done()
|
||||
console.log('') // 换行
|
||||
}
|
||||
|
||||
const uploadDist = async () => {
|
||||
try {
|
||||
const platform = process.platform
|
||||
if (configList[platform]) {
|
||||
const uploadedVersionFiles = new Set()
|
||||
for (const [index, config] of configList[platform].entries()) {
|
||||
const fileName = `${config.appNameWithPrefix}-${VERSION}-${config.arch}.${config.ext}`
|
||||
const filePath = path.join(distPath, fileName)
|
||||
const versionFilePath = path.join(distPath, config['version-file'])
|
||||
let versionFileName = config['version-file']
|
||||
if (VERSION.toLocaleLowerCase().includes('beta')) {
|
||||
versionFileName = versionFileName.replace('.yml', '.beta.yml')
|
||||
}
|
||||
console.log('[PicGo Dist] Preparing to upload', fileName)
|
||||
const client = new S3Client(S3Options)
|
||||
if (fs.existsSync(filePath)) {
|
||||
const uploadDistToS3 = new Upload({
|
||||
client,
|
||||
params: {
|
||||
Bucket: S3_BUCKET,
|
||||
Key: `${FILE_PATH}${fileName}`,
|
||||
Body: fs.createReadStream(filePath),
|
||||
ContentType: 'application/octet-stream'
|
||||
}
|
||||
})
|
||||
// upload dist file
|
||||
console.log('[PicGo Dist] Uploading...', fileName, `${index + 1}/${configList[platform].length}`)
|
||||
uploadDistToS3.on('httpUploadProgress', progress => {
|
||||
console.log(`[PicGo Dist] Uploading... ${progress.loaded}/${progress.total}`)
|
||||
})
|
||||
await uploadDistToS3.done()
|
||||
} else {
|
||||
console.warn('[PicGo Dist] File not found:', fileName)
|
||||
}
|
||||
const configs = getUploadConfigs()
|
||||
|
||||
// upload version file
|
||||
if (!uploadedVersionFiles.has(versionFileName) && fs.existsSync(versionFilePath)) {
|
||||
const uploadVersionFileToS3 = new Upload({
|
||||
client,
|
||||
params: {
|
||||
Bucket: S3_BUCKET,
|
||||
Key: `${versionFileName}`,
|
||||
Body: fs.createReadStream(versionFilePath),
|
||||
ContentType: mime.lookup(versionFileName)
|
||||
}
|
||||
})
|
||||
// upload to legacy bucket as well
|
||||
// will be deprecated in 2.5.0
|
||||
const legacyClient = new S3Client(S3LegacyOptions)
|
||||
const uploadVersionFileToLegacyS3 = new Upload({
|
||||
client: legacyClient,
|
||||
params: {
|
||||
Bucket: S3_LEGACY_BUCKET,
|
||||
Key: `${versionFileName}`,
|
||||
Body: fs.createReadStream(versionFilePath),
|
||||
ContentType: mime.lookup(versionFileName)
|
||||
}
|
||||
})
|
||||
console.log('[PicGo Version File] Uploading...', versionFileName)
|
||||
await uploadVersionFileToS3.done()
|
||||
await uploadVersionFileToLegacyS3.done()
|
||||
uploadedVersionFiles.add(versionFileName)
|
||||
console.log('[PicGo Version File] Upload successfully')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('platform not supported!', platform)
|
||||
if (configs.length === 0) {
|
||||
console.warn('[PicGo] No upload config found!')
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`[PicGo] Upload mode: ${shouldUploadAll() ? 'ALL PLATFORMS' : process.platform}`)
|
||||
console.log(`[PicGo] Version: ${VERSION}`)
|
||||
console.log(`[PicGo] Total files to upload: ${configs.length}\n`)
|
||||
|
||||
const uploadedVersionFiles = new Set()
|
||||
const client = new S3Client(S3Options)
|
||||
const legacyClient = new S3Client(S3LegacyOptions)
|
||||
|
||||
for (const [index, config] of configs.entries()) {
|
||||
const fileName = `${config.appNameWithPrefix}-${VERSION}-${config.arch}.${config.ext}`
|
||||
const filePath = path.join(distPath, fileName)
|
||||
let versionFileName = config['version-file']
|
||||
|
||||
console.log(`[${index + 1}/${configs.length}] Processing ${fileName}`)
|
||||
|
||||
// 上传构建产物
|
||||
if (fs.existsSync(filePath)) {
|
||||
console.log(` Uploading to S3: ${FILE_PATH}${fileName}`)
|
||||
await uploadFileToS3(client, S3_BUCKET, `${FILE_PATH}${fileName}`, filePath)
|
||||
console.log(` ✅ Uploaded: ${fileName}`)
|
||||
} else {
|
||||
console.warn(` ⚠️ File not found: ${fileName}`)
|
||||
}
|
||||
|
||||
let versionFilePath = path.join(distPath, versionFileName)
|
||||
// Beta 版本使用不同的 yml 文件名
|
||||
if (VERSION.toLowerCase().includes('beta') && fs.existsSync(versionFilePath)) {
|
||||
versionFileName = versionFileName.replace('.yml', '.beta.yml')
|
||||
const betaVersionFilePath = path.join(distPath, versionFileName)
|
||||
// change to beta version file path
|
||||
fs.renameSync(versionFilePath, betaVersionFilePath)
|
||||
versionFilePath = betaVersionFilePath
|
||||
}
|
||||
|
||||
// 上传版本文件(每个 yml 只上传一次)
|
||||
if (!uploadedVersionFiles.has(versionFileName) && fs.existsSync(versionFilePath)) {
|
||||
console.log(` Uploading version file: ${versionFileName}`)
|
||||
const versionFileNameFinal = uploadToDev ? `${DEV_DIST_PREFIX}${versionFileName}` : versionFileName
|
||||
|
||||
// 上传到主 bucket
|
||||
await uploadFileToS3(
|
||||
client,
|
||||
S3_BUCKET,
|
||||
versionFileNameFinal,
|
||||
versionFilePath,
|
||||
mime.lookup(versionFileName) || 'text/yaml'
|
||||
)
|
||||
|
||||
// 上传到 legacy bucket
|
||||
await uploadFileToS3(
|
||||
legacyClient,
|
||||
S3_LEGACY_BUCKET,
|
||||
versionFileNameFinal,
|
||||
versionFilePath,
|
||||
mime.lookup(versionFileName) || 'text/yaml'
|
||||
)
|
||||
|
||||
uploadedVersionFiles.add(versionFileName)
|
||||
console.log(` ✅ Version file uploaded: ${versionFileName}`)
|
||||
}
|
||||
|
||||
console.log('')
|
||||
}
|
||||
|
||||
console.log('[PicGo] 🎉 All uploads completed!')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error('[PicGo] ❌ Upload error:', e)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user