mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2025-12-06 03:55:37 -05:00
feat: Started on updates and workflows
This commit is contained in:
135
.github/workflows/build-release.yml
vendored
Normal file
135
.github/workflows/build-release.yml
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
name: 'Build and Release CreamLinux'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Required for semantic-release
|
||||
|
||||
- name: Set up semantic-release environment
|
||||
run: |
|
||||
echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV
|
||||
# More environment setup for release if needed
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run ESLint
|
||||
run: npm run lint
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: 'src-tauri -> target'
|
||||
cache-on-failure: true
|
||||
|
||||
# Setup platform-specific dependencies
|
||||
- name: Install Linux dependencies
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install macOS dependencies
|
||||
if: matrix.platform == 'macos-latest'
|
||||
run: |
|
||||
rustup target add aarch64-apple-darwin
|
||||
|
||||
- name: Install Windows dependencies
|
||||
if: matrix.platform == 'windows-latest'
|
||||
run: |
|
||||
# Windows typically doesn't need additional dependencies
|
||||
|
||||
# Sync package version
|
||||
- name: Sync Version
|
||||
run: npm run sync-version
|
||||
|
||||
# Run semantic-release only on the ubuntu runner to avoid conflicts
|
||||
- name: Semantic Release
|
||||
if: matrix.platform == 'ubuntu-latest' && github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
run: npx semantic-release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Build the app with updater artifacts
|
||||
- name: Build the app
|
||||
run: npm run tauri build
|
||||
|
||||
# Upload the artifacts for each platform
|
||||
- name: Upload Linux artifacts
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: creamlinux-linux
|
||||
path: |
|
||||
src-tauri/target/release/bundle/deb/*.deb
|
||||
src-tauri/target/release/bundle/appimage/*.AppImage
|
||||
src-tauri/target/release/bundle/appimage/*.AppImage.sig
|
||||
|
||||
- name: Upload macOS artifacts
|
||||
if: matrix.platform == 'macos-latest'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: creamlinux-macos
|
||||
path: |
|
||||
src-tauri/target/release/bundle/macos/*.app
|
||||
src-tauri/target/release/bundle/macos/*.app.tar.gz
|
||||
src-tauri/target/release/bundle/macos/*.app.tar.gz.sig
|
||||
src-tauri/target/release/bundle/dmg/*.dmg
|
||||
|
||||
- name: Upload Windows artifacts
|
||||
if: matrix.platform == 'windows-latest'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: creamlinux-windows
|
||||
path: |
|
||||
src-tauri/target/release/bundle/msi/*.msi
|
||||
src-tauri/target/release/bundle/msi/*.msi.sig
|
||||
src-tauri/target/release/bundle/nsis/*.exe
|
||||
src-tauri/target/release/bundle/nsis/*.exe.sig
|
||||
|
||||
# Generate updater JSON file (run only on ubuntu)
|
||||
- name: Generate updater JSON
|
||||
if: matrix.platform == 'ubuntu-latest' && github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
node scripts/generate-updater-json.js
|
||||
|
||||
# Create GitHub release with all artifacts
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: matrix.platform == 'ubuntu-latest' && github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
with:
|
||||
files: |
|
||||
latest.json
|
||||
src-tauri/target/release/bundle/**/*.{AppImage,AppImage.sig,deb,dmg,app.tar.gz,app.tar.gz.sig,msi,msi.sig,exe,exe.sig}
|
||||
draft: false
|
||||
prerelease: false
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
generate_release_notes: true
|
||||
59
.github/workflows/build.yml
vendored
59
.github/workflows/build.yml
vendored
@@ -1,59 +0,0 @@
|
||||
name: 'Build CreamLinux'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build-tauri:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ubuntu-latest]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: 'src-tauri -> target'
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Run ESLint
|
||||
run: npm run lint
|
||||
|
||||
- name: Build the app
|
||||
run: npm run tauri build
|
||||
|
||||
- name: Upload binary artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: creamlinux-${{ runner.os }}
|
||||
path: |
|
||||
src-tauri/target/release/creamlinux
|
||||
src-tauri/target/release/bundle/deb/*.deb
|
||||
src-tauri/target/release/bundle/appimage/*.AppImage
|
||||
31
.releaserc.js
Normal file
31
.releaserc.js
Normal file
@@ -0,0 +1,31 @@
|
||||
module.exports = {
|
||||
branches: ['main'],
|
||||
plugins: [
|
||||
'@semantic-release/commit-analyzer',
|
||||
'@semantic-release/release-notes-generator',
|
||||
'@semantic-release/changelog',
|
||||
[
|
||||
'@semantic-release/npm',
|
||||
{
|
||||
// Don't actually publish to npm
|
||||
npmPublish: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
'@semantic-release/git',
|
||||
{
|
||||
assets: ['package.json', 'CHANGELOG.md'],
|
||||
message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}',
|
||||
},
|
||||
],
|
||||
[
|
||||
'@semantic-release/github',
|
||||
{
|
||||
assets: [
|
||||
{ path: 'latest.json', label: 'Updater manifest file' },
|
||||
// We don't need to specify release assets here as they're handled by the GitHub release action
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
||||
6268
package-lock.json
generated
6268
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -9,10 +9,18 @@
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"optimize-svg": "node scripts/optimize-svg.js"
|
||||
"optimize-svg": "node scripts/optimize-svg.js",
|
||||
"sync-version": "node scripts/sync-version.js",
|
||||
"generate-updater": "node scripts/generate-updater-json.js",
|
||||
"update-tauri-config": "node scripts/update-tauri-config.js",
|
||||
"update-api-changelog": "node scripts/api-changelog.js",
|
||||
"prepare-release": "npm run sync-version && npm run update-api-changelog && npm run update-tauri-config",
|
||||
"release": "semantic-release"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
"@tauri-apps/plugin-process": "^2.2.1",
|
||||
"@tauri-apps/plugin-updater": "^2.7.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"sass": "^1.89.0",
|
||||
@@ -20,6 +28,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"@semantic-release/github": "^11.0.2",
|
||||
"@svgr/core": "^8.1.0",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@tauri-apps/cli": "^2.5.0",
|
||||
@@ -32,6 +43,7 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"sass-embedded": "^1.86.3",
|
||||
"semantic-release": "^24.2.4",
|
||||
"typescript": "~5.7.2",
|
||||
"typescript-eslint": "^8.26.1",
|
||||
"vite": "^6.3.1",
|
||||
|
||||
215
scripts/api-changelog.js
Normal file
215
scripts/api-changelog.js
Normal file
@@ -0,0 +1,215 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
// Define paths
|
||||
const rustFilesPath = path.join(__dirname, '..', 'src-tauri')
|
||||
|
||||
function getApiDefinitions() {
|
||||
// Get a list of all Rust files
|
||||
const rustFiles = findRustFiles(rustFilesPath)
|
||||
|
||||
// Extract API functions and structs from Rust files
|
||||
const apiDefinitions = {
|
||||
commands: [],
|
||||
structs: [],
|
||||
}
|
||||
|
||||
rustFiles.forEach((filePath) => {
|
||||
const content = fs.readFileSync(filePath, 'utf8')
|
||||
|
||||
// Find Tauri commands (API endpoints)
|
||||
const commandRegex = /#\[tauri::command\]\s+(?:pub\s+)?(?:async\s+)?fn\s+([a-zA-Z0-9_]+)/g
|
||||
let match
|
||||
while ((match = commandRegex.exec(content)) !== null) {
|
||||
apiDefinitions.commands.push({
|
||||
name: match[1],
|
||||
file: path.relative(rustFilesPath, filePath),
|
||||
})
|
||||
}
|
||||
|
||||
// Find structs that are likely part of the API
|
||||
const structRegex = /#\[derive\(.*Serialize.*\)\]\s+(?:pub\s+)?struct\s+([a-zA-Z0-9_]+)/g
|
||||
while ((match = structRegex.exec(content)) !== null) {
|
||||
apiDefinitions.structs.push({
|
||||
name: match[1],
|
||||
file: path.relative(rustFilesPath, filePath),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return apiDefinitions
|
||||
}
|
||||
|
||||
function findRustFiles(dir) {
|
||||
let results = []
|
||||
const list = fs.readdirSync(dir)
|
||||
|
||||
list.forEach((file) => {
|
||||
const filePath = path.join(dir, file)
|
||||
const stat = fs.statSync(filePath)
|
||||
|
||||
if (stat && stat.isDirectory() && file !== 'target') {
|
||||
// Recursively search subdirectories, but skip the 'target' directory
|
||||
results = results.concat(findRustFiles(filePath))
|
||||
} else if (path.extname(file) === '.rs') {
|
||||
// Add Rust files to the results
|
||||
results.push(filePath)
|
||||
}
|
||||
})
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
function compareApiDefinitions(oldApi, newApi) {
|
||||
const changes = {
|
||||
commands: {
|
||||
added: [],
|
||||
removed: [],
|
||||
},
|
||||
structs: {
|
||||
added: [],
|
||||
removed: [],
|
||||
},
|
||||
}
|
||||
|
||||
// Find added and removed commands
|
||||
const oldCommandNames = oldApi.commands.map((cmd) => cmd.name)
|
||||
const newCommandNames = newApi.commands.map((cmd) => cmd.name)
|
||||
|
||||
changes.commands.added = newApi.commands.filter((cmd) => !oldCommandNames.includes(cmd.name))
|
||||
changes.commands.removed = oldApi.commands.filter((cmd) => !newCommandNames.includes(cmd.name))
|
||||
|
||||
// Find added and removed structs
|
||||
const oldStructNames = oldApi.structs.map((struct) => struct.name)
|
||||
const newStructNames = newApi.structs.map((struct) => struct.name)
|
||||
|
||||
changes.structs.added = newApi.structs.filter((struct) => !oldStructNames.includes(struct.name))
|
||||
changes.structs.removed = oldApi.structs.filter((struct) => !newStructNames.includes(struct.name))
|
||||
|
||||
return changes
|
||||
}
|
||||
|
||||
function updateChangelog(changes) {
|
||||
const changelogPath = path.join(__dirname, '..', 'CHANGELOG.md')
|
||||
let changelog = ''
|
||||
|
||||
// Create changelog if it doesn't exist
|
||||
if (fs.existsSync(changelogPath)) {
|
||||
changelog = fs.readFileSync(changelogPath, 'utf8')
|
||||
} else {
|
||||
changelog =
|
||||
'# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n'
|
||||
}
|
||||
|
||||
// Get the current version and date
|
||||
const packageJson = require(path.join(__dirname, '..', 'package.json'))
|
||||
const version = packageJson.version
|
||||
const date = new Date().toISOString().split('T')[0]
|
||||
|
||||
// Create the new changelog entry
|
||||
let newEntry = `## [${version}] - ${date}\n\n`
|
||||
|
||||
if (
|
||||
changes.commands.added.length > 0 ||
|
||||
changes.commands.removed.length > 0 ||
|
||||
changes.structs.added.length > 0 ||
|
||||
changes.structs.removed.length > 0
|
||||
) {
|
||||
newEntry += '### API Changes\n\n'
|
||||
|
||||
if (changes.commands.added.length > 0) {
|
||||
newEntry += '#### New Commands\n\n'
|
||||
changes.commands.added.forEach((cmd) => {
|
||||
newEntry += `- \`${cmd.name}\` in \`${cmd.file}\`\n`
|
||||
})
|
||||
newEntry += '\n'
|
||||
}
|
||||
|
||||
if (changes.commands.removed.length > 0) {
|
||||
newEntry += '#### Removed Commands\n\n'
|
||||
changes.commands.removed.forEach((cmd) => {
|
||||
newEntry += `- \`${cmd.name}\` (was in \`${cmd.file}\`)\n`
|
||||
})
|
||||
newEntry += '\n'
|
||||
}
|
||||
|
||||
if (changes.structs.added.length > 0) {
|
||||
newEntry += '#### New Structures\n\n'
|
||||
changes.structs.added.forEach((struct) => {
|
||||
newEntry += `- \`${struct.name}\` in \`${struct.file}\`\n`
|
||||
})
|
||||
newEntry += '\n'
|
||||
}
|
||||
|
||||
if (changes.structs.removed.length > 0) {
|
||||
newEntry += '#### Removed Structures\n\n'
|
||||
changes.structs.removed.forEach((struct) => {
|
||||
newEntry += `- \`${struct.name}\` (was in \`${struct.file}\`)\n`
|
||||
})
|
||||
newEntry += '\n'
|
||||
}
|
||||
} else {
|
||||
newEntry += 'No API changes in this release.\n\n'
|
||||
}
|
||||
|
||||
// Add git commit information since last release
|
||||
try {
|
||||
// Get the latest tag
|
||||
const latestTag = execSync('git describe --tags --abbrev=0', { encoding: 'utf8' }).trim()
|
||||
|
||||
// Get commits since the latest tag
|
||||
const commits = execSync(`git log ${latestTag}..HEAD --pretty=format:"%h %s (%an)"`, {
|
||||
encoding: 'utf8',
|
||||
}).trim()
|
||||
|
||||
if (commits) {
|
||||
newEntry += '### Other Changes\n\n'
|
||||
|
||||
// Split commits by line and add them to the changelog
|
||||
commits.split('\n').forEach((commit) => {
|
||||
newEntry += `- ${commit}\n`
|
||||
})
|
||||
|
||||
newEntry += '\n'
|
||||
}
|
||||
} catch (error) {
|
||||
// If there are no tags or other git issues, just continue without commit info
|
||||
console.warn('Could not get git commit information:', error.message)
|
||||
}
|
||||
|
||||
// Add the new entry to the changelog
|
||||
if (changelog.includes('## [')) {
|
||||
// Insert new entry after the first heading
|
||||
changelog = changelog.replace('# Changelog\n\n', `# Changelog\n\n${newEntry}`)
|
||||
} else {
|
||||
// Append to the end if no existing versions
|
||||
changelog += newEntry
|
||||
}
|
||||
|
||||
// Write the updated changelog
|
||||
fs.writeFileSync(changelogPath, changelog)
|
||||
console.log(`Updated CHANGELOG.md with API changes for version ${version}`)
|
||||
}
|
||||
|
||||
// Main execution
|
||||
try {
|
||||
// Check if we have a saved API definition
|
||||
const apiCachePath = path.join(__dirname, '.api-cache.json')
|
||||
let oldApi = { commands: [], structs: [] }
|
||||
let newApi = getApiDefinitions()
|
||||
|
||||
if (fs.existsSync(apiCachePath)) {
|
||||
oldApi = JSON.parse(fs.readFileSync(apiCachePath, 'utf8'))
|
||||
}
|
||||
|
||||
// Compare and update the changelog
|
||||
const changes = compareApiDefinitions(oldApi, newApi)
|
||||
updateChangelog(changes)
|
||||
|
||||
// Save the new API definition for next time
|
||||
fs.writeFileSync(apiCachePath, JSON.stringify(newApi, null, 2))
|
||||
} catch (error) {
|
||||
console.error('Error updating API changelog:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
82
scripts/generate-updater-json.js
Normal file
82
scripts/generate-updater-json.js
Normal file
@@ -0,0 +1,82 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
// Read the current version from package.json
|
||||
const packageJson = require('../package.json')
|
||||
const version = packageJson.version
|
||||
console.log(`Current version: ${version}`)
|
||||
|
||||
// Get the current date in RFC 3339 format for pub_date
|
||||
const pubDate = new Date().toISOString()
|
||||
|
||||
// Base URL where the assets will be available
|
||||
const baseUrl = 'https://github.com/tickbase/creamlinux/releases/download'
|
||||
const releaseTag = `v${version}`
|
||||
const releaseUrl = `${baseUrl}/${releaseTag}`
|
||||
|
||||
// Create the updater JSON structure
|
||||
const updaterJson = {
|
||||
version,
|
||||
notes: `Release version ${version}`,
|
||||
pub_date: pubDate,
|
||||
platforms: {
|
||||
// Windows x64
|
||||
'windows-x86_64': {
|
||||
url: `${releaseUrl}/creamlinux-setup.exe`,
|
||||
signature: readSignature('windows', 'creamlinux-setup.exe'),
|
||||
},
|
||||
// Linux x64
|
||||
'linux-x86_64': {
|
||||
url: `${releaseUrl}/creamlinux.AppImage`,
|
||||
signature: readSignature('linux', 'creamlinux.AppImage'),
|
||||
},
|
||||
// macOS x64 and arm64 (universal)
|
||||
'darwin-universal': {
|
||||
url: `${releaseUrl}/creamlinux.app.tar.gz`,
|
||||
signature: readSignature('macos', 'creamlinux.app.tar.gz'),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Write the updater JSON file
|
||||
fs.writeFileSync('latest.json', JSON.stringify(updaterJson, null, 2))
|
||||
console.log('Created latest.json updater file')
|
||||
|
||||
// Helper function to read signature files
|
||||
function readSignature(platform, filename) {
|
||||
try {
|
||||
// Determine path based on platform
|
||||
let sigPath
|
||||
|
||||
switch (platform) {
|
||||
case 'windows':
|
||||
// Check both NSIS and MSI
|
||||
try {
|
||||
sigPath = path.join('src-tauri', 'target', 'release', 'bundle', 'nsis', `${filename}.sig`)
|
||||
return fs.readFileSync(sigPath, 'utf8').trim()
|
||||
} catch (e) {
|
||||
sigPath = path.join('src-tauri', 'target', 'release', 'bundle', 'msi', `${filename}.sig`)
|
||||
return fs.readFileSync(sigPath, 'utf8').trim()
|
||||
}
|
||||
case 'linux':
|
||||
sigPath = path.join(
|
||||
'src-tauri',
|
||||
'target',
|
||||
'release',
|
||||
'bundle',
|
||||
'appimage',
|
||||
`${filename}.sig`
|
||||
)
|
||||
return fs.readFileSync(sigPath, 'utf8').trim()
|
||||
case 'macos':
|
||||
sigPath = path.join('src-tauri', 'target', 'release', 'bundle', 'macos', `${filename}.sig`)
|
||||
return fs.readFileSync(sigPath, 'utf8').trim()
|
||||
default:
|
||||
throw new Error(`Unknown platform: ${platform}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error reading signature for ${platform}/${filename}:`, error.message)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
29
scripts/sync-version.js
Normal file
29
scripts/sync-version.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
// Read current version from package.json
|
||||
const packageJsonPath = path.join(__dirname, '..', 'package.json')
|
||||
const packageJson = require(packageJsonPath)
|
||||
const version = packageJson.version
|
||||
|
||||
console.log(`Current version: ${version}`)
|
||||
|
||||
// Update Cargo.toml
|
||||
const cargoTomlPath = path.join(__dirname, '..', 'src-tauri', 'Cargo.toml')
|
||||
let cargoToml = fs.readFileSync(cargoTomlPath, 'utf8')
|
||||
|
||||
// Replace the version in Cargo.toml
|
||||
cargoToml = cargoToml.replace(/version\s*=\s*"[^"]+"/m, `version = "${version}"`)
|
||||
fs.writeFileSync(cargoTomlPath, cargoToml)
|
||||
console.log(`Updated Cargo.toml version to ${version}`)
|
||||
|
||||
// Update tauri.conf.json
|
||||
const tauriConfigPath = path.join(__dirname, '..', 'src-tauri', 'tauri.conf.json')
|
||||
const tauriConfig = JSON.parse(fs.readFileSync(tauriConfigPath, 'utf8'))
|
||||
|
||||
// Set the version in tauri.conf.json
|
||||
tauriConfig.version = version
|
||||
fs.writeFileSync(tauriConfigPath, JSON.stringify(tauriConfig, null, 2))
|
||||
console.log(`Updated tauri.conf.json version to ${version}`)
|
||||
|
||||
console.log('Version synchronization completed!')
|
||||
42
scripts/update-tauri-config.js
Normal file
42
scripts/update-tauri-config.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
// Path to your tauri.conf.json file
|
||||
const tauriConfigPath = path.join(__dirname, '..', 'src-tauri', 'tauri.conf.json')
|
||||
|
||||
// Read the current config
|
||||
const rawConfig = fs.readFileSync(tauriConfigPath, 'utf8')
|
||||
const config = JSON.parse(rawConfig)
|
||||
|
||||
// Add or update the updater configuration
|
||||
if (!config.plugins) {
|
||||
config.plugins = {}
|
||||
}
|
||||
|
||||
// Get the public key from environment variable
|
||||
const pubkey = process.env.TAURI_PUBLIC_KEY || ''
|
||||
|
||||
if (!pubkey) {
|
||||
console.warn(
|
||||
'Warning: TAURI_PUBLIC_KEY environment variable is not set. Updater will not work correctly!'
|
||||
)
|
||||
}
|
||||
|
||||
// Configure the updater plugin
|
||||
config.plugins.updater = {
|
||||
pubkey,
|
||||
endpoints: ['https://github.com/tickbase/creamlinux/releases/latest/download/latest.json'],
|
||||
}
|
||||
|
||||
// Configure bundle settings for updater artifacts
|
||||
if (!config.bundle) {
|
||||
config.bundle = {}
|
||||
}
|
||||
|
||||
// Set createUpdaterArtifacts to true
|
||||
config.bundle.createUpdaterArtifacts = true
|
||||
|
||||
// Write the updated config back to the file
|
||||
fs.writeFileSync(tauriConfigPath, JSON.stringify(config, null, 2))
|
||||
|
||||
console.log('Tauri config updated with updater configuration')
|
||||
@@ -30,6 +30,10 @@ tauri-plugin-shell = "2.0.0-rc"
|
||||
tauri-plugin-dialog = "2.0.0-rc"
|
||||
tauri-plugin-fs = "2.0.0-rc"
|
||||
num_cpus = "1.16.0"
|
||||
tauri-plugin-process = "2"
|
||||
|
||||
[features]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-updater = "2"
|
||||
|
||||
14
src-tauri/capabilities/desktop.json
Normal file
14
src-tauri/capabilities/desktop.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"identifier": "desktop-capability",
|
||||
"platforms": [
|
||||
"macOS",
|
||||
"windows",
|
||||
"linux"
|
||||
],
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"updater:default"
|
||||
]
|
||||
}
|
||||
@@ -512,6 +512,8 @@ fn main() {
|
||||
};
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
|
||||
@@ -4,19 +4,25 @@
|
||||
"frontendDist": "../dist",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"beforeBuildCommand": "npm run build"
|
||||
"beforeBuildCommand": "npm run sync-version && npm run build"
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"category": "Utility",
|
||||
"createUpdaterArtifacts": true,
|
||||
"icon": ["icons/128x128.png", "icons/128x128@2x.png", "icons/icon.png"]
|
||||
},
|
||||
"productName": "Creamlinux",
|
||||
"mainBinaryName": "creamlinux",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.creamlinux.dev",
|
||||
"plugins": {},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDJDNEI1NzBBRDUxODQ3RjEKUldUeFJ4alZDbGRMTE5Vc241NG5yL080UklnaW1iUGdUWElPRXloRGtKZ3M2SWkzK0RGSDh3Q2kK",
|
||||
"endpoints": ["https://github.com/Novattz/rust-gui-dev/releases/latest/download/latest.json"]
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": false,
|
||||
"windows": [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useAppContext } from '@/contexts/useAppContext'
|
||||
import { UpdateChecker } from '@/components/updater'
|
||||
import { useAppLogic } from '@/hooks'
|
||||
import './styles/main.scss'
|
||||
|
||||
@@ -104,6 +105,7 @@ function App() {
|
||||
onClose={handleDlcDialogClose}
|
||||
onConfirm={handleDlcConfirm}
|
||||
/>
|
||||
<UpdateChecker />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
|
||||
147
src/components/updater/UpdateChecker.tsx
Normal file
147
src/components/updater/UpdateChecker.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { check, type Update, type DownloadEvent } from '@tauri-apps/plugin-updater'
|
||||
import { relaunch } from '@tauri-apps/plugin-process'
|
||||
import { Button } from '@/components/buttons'
|
||||
|
||||
/**
|
||||
* React component that checks for updates and provides
|
||||
* UI for downloading and installing them
|
||||
*/
|
||||
const UpdateChecker = () => {
|
||||
const [updateAvailable, setUpdateAvailable] = useState(false)
|
||||
const [updateInfo, setUpdateInfo] = useState<Update | null>(null)
|
||||
const [isChecking, setIsChecking] = useState(false)
|
||||
const [isDownloading, setIsDownloading] = useState(false)
|
||||
const [downloadProgress, setDownloadProgress] = useState(0)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Check for updates on component mount
|
||||
useEffect(() => {
|
||||
checkForUpdates()
|
||||
}, [])
|
||||
|
||||
const checkForUpdates = async () => {
|
||||
try {
|
||||
setIsChecking(true)
|
||||
setError(null)
|
||||
|
||||
// Check for updates
|
||||
const update = await check()
|
||||
|
||||
if (update) {
|
||||
console.log(`Update available: ${update.version}`)
|
||||
setUpdateAvailable(true)
|
||||
setUpdateInfo(update)
|
||||
} else {
|
||||
console.log('No updates available')
|
||||
setUpdateAvailable(false)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to check for updates:', err)
|
||||
setError(`Failed to check for updates: ${err instanceof Error ? err.message : String(err)}`)
|
||||
} finally {
|
||||
setIsChecking(false)
|
||||
}
|
||||
}
|
||||
|
||||
const downloadAndInstallUpdate = async () => {
|
||||
if (!updateInfo) return
|
||||
|
||||
try {
|
||||
setIsDownloading(true)
|
||||
setError(null)
|
||||
|
||||
let downloaded = 0
|
||||
let contentLength = 0
|
||||
|
||||
// Download and install update
|
||||
await updateInfo.downloadAndInstall((event: DownloadEvent) => {
|
||||
switch (event.event) {
|
||||
case 'Started':
|
||||
// Started event includes contentLength
|
||||
if ('contentLength' in event.data && typeof event.data.contentLength === 'number') {
|
||||
contentLength = event.data.contentLength
|
||||
console.log(`Started downloading ${contentLength} bytes`)
|
||||
}
|
||||
break
|
||||
case 'Progress':
|
||||
// Progress event includes chunkLength
|
||||
if ('chunkLength' in event.data && typeof event.data.chunkLength === 'number' && contentLength > 0) {
|
||||
downloaded += event.data.chunkLength
|
||||
const progress = (downloaded / contentLength) * 100
|
||||
setDownloadProgress(progress)
|
||||
console.log(`Downloaded ${downloaded} from ${contentLength}`)
|
||||
}
|
||||
break
|
||||
case 'Finished':
|
||||
console.log('Download finished')
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
console.log('Update installed, relaunching application')
|
||||
await relaunch()
|
||||
} catch (err) {
|
||||
console.error('Failed to download and install update:', err)
|
||||
setError(`Failed to download and install update: ${err instanceof Error ? err.message : String(err)}`)
|
||||
setIsDownloading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (isChecking) {
|
||||
return <div className="update-checker">Checking for updates...</div>
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="update-checker error">
|
||||
<p>{error}</p>
|
||||
<Button variant="primary" onClick={checkForUpdates}>Try Again</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!updateAvailable || !updateInfo) {
|
||||
return null // Don't show anything if there's no update
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="update-checker">
|
||||
<div className="update-info">
|
||||
<h3>Update Available</h3>
|
||||
<p>Version {updateInfo.version} is available to download.</p>
|
||||
{updateInfo.body && <p className="update-notes">{updateInfo.body}</p>}
|
||||
</div>
|
||||
|
||||
{isDownloading ? (
|
||||
<div className="update-progress">
|
||||
<div className="progress-bar-container">
|
||||
<div
|
||||
className="progress-bar"
|
||||
style={{ width: `${downloadProgress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p>Downloading: {Math.round(downloadProgress)}%</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="update-actions">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={downloadAndInstallUpdate}
|
||||
disabled={isDownloading}
|
||||
>
|
||||
Download & Install
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setUpdateAvailable(false)}
|
||||
>
|
||||
Later
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateChecker
|
||||
1
src/components/updater/index.ts
Normal file
1
src/components/updater/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as UpdateChecker } from './UpdateChecker'
|
||||
@@ -1 +1,2 @@
|
||||
@forward './loading';
|
||||
@forward './updater';
|
||||
|
||||
85
src/styles/components/common/_updater.scss
Normal file
85
src/styles/components/common/_updater.scss
Normal file
@@ -0,0 +1,85 @@
|
||||
@use '../../themes/index' as *;
|
||||
@use '../../abstracts/index' as *;
|
||||
|
||||
/*
|
||||
Update checker component styles
|
||||
*/
|
||||
.update-checker {
|
||||
border-radius: var(--radius-md);
|
||||
background-color: var(--elevated-bg);
|
||||
padding: 1.25rem;
|
||||
margin: 1rem 0;
|
||||
border: 1px solid var(--border-soft);
|
||||
box-shadow: var(--shadow-standard);
|
||||
max-width: 500px;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: var(--z-modal) - 1;
|
||||
|
||||
&.error {
|
||||
border-color: var(--danger);
|
||||
background-color: var(--danger-soft);
|
||||
}
|
||||
|
||||
.update-info {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: var(--bold);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.update-notes {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-soft);
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: var(--radius-sm);
|
||||
white-space: pre-line;
|
||||
margin-top: 0.5rem;
|
||||
@include custom-scrollbar;
|
||||
}
|
||||
}
|
||||
|
||||
.update-progress {
|
||||
margin-top: 1rem;
|
||||
|
||||
.progress-bar-container {
|
||||
height: 6px;
|
||||
background-color: var(--border-soft);
|
||||
border-radius: 3px;
|
||||
margin-bottom: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.update-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user