aegis-framework 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +239 -0
- package/aegis/__init__.py +19 -0
- package/aegis/__pycache__/__init__.cpython-312.pyc +0 -0
- package/aegis/builder/__init__.py +5 -0
- package/aegis/builder/__pycache__/__init__.cpython-312.pyc +0 -0
- package/aegis/builder/__pycache__/builder.cpython-312.pyc +0 -0
- package/aegis/builder/builder.py +301 -0
- package/aegis/cli/__init__.py +5 -0
- package/aegis/cli/__pycache__/__init__.cpython-312.pyc +0 -0
- package/aegis/cli/__pycache__/cli.cpython-312.pyc +0 -0
- package/aegis/cli/cli.py +607 -0
- package/aegis/core/__init__.py +16 -0
- package/aegis/core/__pycache__/__init__.cpython-312.pyc +0 -0
- package/aegis/core/__pycache__/aegis.cpython-312.pyc +0 -0
- package/aegis/core/__pycache__/bridge.cpython-312.pyc +0 -0
- package/aegis/core/__pycache__/window.cpython-312.pyc +0 -0
- package/aegis/core/aegis.py +97 -0
- package/aegis/core/bridge.py +270 -0
- package/aegis/core/preload.py +160 -0
- package/aegis/core/window.py +603 -0
- package/aegis/runtime/__init__.py +1 -0
- package/aegis/runtime/aegis-api.js +519 -0
- package/aegis-cli.py +18 -0
- package/bin/aegis +81 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Diego
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# ⚡ Aegis Framework
|
|
2
|
+
|
|
3
|
+
**A lightweight alternative to Electron for Linux desktop apps.**
|
|
4
|
+
|
|
5
|
+
Create desktop applications with HTML, CSS, and JavaScript that package into ~200KB AppImages instead of 150MB!
|
|
6
|
+
|
|
7
|
+
## 📊 Size Comparison
|
|
8
|
+
|
|
9
|
+
| Framework | Typical App Size | Aegis App |
|
|
10
|
+
|-----------|-----------------|-----------|
|
|
11
|
+
| **Electron** | 150-200 MB | - |
|
|
12
|
+
| **Aegis** | - | **~200 KB** |
|
|
13
|
+
|
|
14
|
+
**That's 700x smaller!**
|
|
15
|
+
|
|
16
|
+
## 🚀 Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Install globally via npm
|
|
20
|
+
npm install -g aegis-framework
|
|
21
|
+
|
|
22
|
+
# Or use npx without installing
|
|
23
|
+
npx aegis-framework init my-app
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### System Requirements (Linux only)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Ubuntu/Debian
|
|
30
|
+
sudo apt install python3 python3-gi gir1.2-webkit2-4.1
|
|
31
|
+
|
|
32
|
+
# Fedora
|
|
33
|
+
sudo dnf install python3 python3-gobject webkit2gtk4.1
|
|
34
|
+
|
|
35
|
+
# Arch
|
|
36
|
+
sudo pacman -S python python-gobject webkit2gtk-4.1
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 🏁 Quick Start
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Create a new project
|
|
43
|
+
aegis init my-app
|
|
44
|
+
|
|
45
|
+
# Enter the directory
|
|
46
|
+
cd my-app
|
|
47
|
+
|
|
48
|
+
# Start development server
|
|
49
|
+
aegis dev
|
|
50
|
+
|
|
51
|
+
# Build AppImage
|
|
52
|
+
aegis build
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 📁 Project Structure
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
my-app/
|
|
59
|
+
├── aegis.config.json # Configuration
|
|
60
|
+
├── src/
|
|
61
|
+
│ ├── index.html # Main HTML
|
|
62
|
+
│ ├── styles.css # Styles
|
|
63
|
+
│ ├── app.js # JavaScript
|
|
64
|
+
│ └── preload.js # Security config
|
|
65
|
+
└── assets/
|
|
66
|
+
└── icon.png # App icon
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 🔌 API Reference
|
|
70
|
+
|
|
71
|
+
### File Operations
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
// Read directory
|
|
75
|
+
const dir = await Aegis.read({ path: '/home/user' });
|
|
76
|
+
console.log(dir.entries);
|
|
77
|
+
|
|
78
|
+
// Read file
|
|
79
|
+
const file = await Aegis.read({ path: '/home/user', file: 'data.txt' });
|
|
80
|
+
console.log(file.content);
|
|
81
|
+
|
|
82
|
+
// Write file
|
|
83
|
+
await Aegis.write({
|
|
84
|
+
path: '/home/user',
|
|
85
|
+
file: 'output.txt',
|
|
86
|
+
content: 'Hello World!'
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Check existence
|
|
90
|
+
const info = await Aegis.exists({ path: '/home/user/file.txt' });
|
|
91
|
+
|
|
92
|
+
// Create directory
|
|
93
|
+
await Aegis.mkdir({ path: '/home/user/new-folder' });
|
|
94
|
+
|
|
95
|
+
// Delete
|
|
96
|
+
await Aegis.remove({ path: '/home/user/old-file.txt' });
|
|
97
|
+
|
|
98
|
+
// Copy
|
|
99
|
+
await Aegis.copy({ src: '/path/from', dest: '/path/to' });
|
|
100
|
+
|
|
101
|
+
// Move/Rename
|
|
102
|
+
await Aegis.move({ src: '/path/from', dest: '/path/to' });
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Dialogs
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
// Message dialog
|
|
109
|
+
await Aegis.dialog.message({
|
|
110
|
+
type: 'info', // info, warning, error, question
|
|
111
|
+
title: 'Hello',
|
|
112
|
+
message: 'World!'
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Confirmation dialog
|
|
116
|
+
const result = await Aegis.dialog.message({
|
|
117
|
+
type: 'question',
|
|
118
|
+
title: 'Confirm',
|
|
119
|
+
message: 'Are you sure?',
|
|
120
|
+
buttons: 'yesno'
|
|
121
|
+
});
|
|
122
|
+
if (result.response) { /* User clicked Yes */ }
|
|
123
|
+
|
|
124
|
+
// Open file dialog
|
|
125
|
+
const file = await Aegis.dialog.open({
|
|
126
|
+
title: 'Select File',
|
|
127
|
+
filters: [{ name: 'Images', extensions: ['png', 'jpg'] }]
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Save file dialog
|
|
131
|
+
const savePath = await Aegis.dialog.save({
|
|
132
|
+
title: 'Save As',
|
|
133
|
+
defaultName: 'document.txt'
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### App Control
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
// Window controls
|
|
141
|
+
await Aegis.app.minimize();
|
|
142
|
+
await Aegis.app.maximize();
|
|
143
|
+
await Aegis.app.quit();
|
|
144
|
+
|
|
145
|
+
// Get system paths (localized!)
|
|
146
|
+
const home = await Aegis.app.getPath({ name: 'home' });
|
|
147
|
+
const docs = await Aegis.app.getPath({ name: 'documents' });
|
|
148
|
+
// Also: desktop, downloads, music, pictures, videos
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Window Control (Frameless)
|
|
152
|
+
|
|
153
|
+
```javascript
|
|
154
|
+
// Make element draggable for window movement
|
|
155
|
+
Aegis.window.moveBar('#titlebar', { exclude: '.btn' });
|
|
156
|
+
|
|
157
|
+
// Setup resize handles
|
|
158
|
+
Aegis.window.resizeHandles({
|
|
159
|
+
'.resize-n': 'n',
|
|
160
|
+
'.resize-s': 's',
|
|
161
|
+
'.resize-se': 'se'
|
|
162
|
+
// etc: e, w, ne, nw, sw
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Get/set size
|
|
166
|
+
const size = await Aegis.window.getSize();
|
|
167
|
+
await Aegis.window.setSize({ width: 1024, height: 768 });
|
|
168
|
+
|
|
169
|
+
// Get/set position
|
|
170
|
+
const pos = await Aegis.window.getPosition();
|
|
171
|
+
await Aegis.window.setPosition({ x: 100, y: 100 });
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Run Commands
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
// Shell command
|
|
178
|
+
const result = await Aegis.run({ sh: 'ls -la' });
|
|
179
|
+
console.log(result.output);
|
|
180
|
+
|
|
181
|
+
// Python code
|
|
182
|
+
const py = await Aegis.run({ py: '2 + 2' });
|
|
183
|
+
console.log(py.output); // "4"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## 🔒 Security (preload.js)
|
|
187
|
+
|
|
188
|
+
Control which APIs are exposed to your frontend:
|
|
189
|
+
|
|
190
|
+
```javascript
|
|
191
|
+
// src/preload.js
|
|
192
|
+
Aegis.expose([
|
|
193
|
+
'read', // Allow file reading
|
|
194
|
+
'write', // Allow file writing
|
|
195
|
+
'dialog', // Allow dialogs
|
|
196
|
+
'app', // Allow app control
|
|
197
|
+
'window', // Allow window control
|
|
198
|
+
// Omit 'run' to prevent command execution!
|
|
199
|
+
]);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## ⚙️ Configuration (aegis.config.json)
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{
|
|
206
|
+
"name": "my-app",
|
|
207
|
+
"title": "My Application",
|
|
208
|
+
"version": "1.0.0",
|
|
209
|
+
"main": "src/index.html",
|
|
210
|
+
"preload": "src/preload.js",
|
|
211
|
+
"width": 1200,
|
|
212
|
+
"height": 800,
|
|
213
|
+
"frame": true,
|
|
214
|
+
"resizable": true,
|
|
215
|
+
"icon": "assets/icon.png"
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Set `"frame": false` for frameless window (custom titlebar).
|
|
220
|
+
|
|
221
|
+
## 🤔 Why Aegis?
|
|
222
|
+
|
|
223
|
+
| Feature | Electron | Aegis |
|
|
224
|
+
|---------|----------|-------|
|
|
225
|
+
| Size | ~150 MB | **~200 KB** |
|
|
226
|
+
| Backend | Node.js | **Python** |
|
|
227
|
+
| Renderer | Chromium (bundled) | **WebKit2GTK (system)** |
|
|
228
|
+
| Platform | Cross-platform | **Linux** |
|
|
229
|
+
| RAM Usage | High | **Low** |
|
|
230
|
+
|
|
231
|
+
Aegis uses the WebKit that's already installed on Linux systems, so apps are tiny!
|
|
232
|
+
|
|
233
|
+
## 📜 License
|
|
234
|
+
|
|
235
|
+
MIT © Diego
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
Made with ❤️ for the Linux community
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Aegis - Lightweight AppImage Framework
|
|
3
|
+
A modern alternative to Electron using WebKit2GTK and Python
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
__author__ = "Diego"
|
|
8
|
+
|
|
9
|
+
# Lazy imports to allow CLI to work without GTK installed
|
|
10
|
+
def __getattr__(name):
|
|
11
|
+
if name == 'AegisApp':
|
|
12
|
+
from aegis.core.aegis import AegisApp
|
|
13
|
+
return AegisApp
|
|
14
|
+
elif name == 'AegisWindow':
|
|
15
|
+
from aegis.core.window import AegisWindow
|
|
16
|
+
return AegisWindow
|
|
17
|
+
raise AttributeError(f"module 'aegis' has no attribute '{name}'")
|
|
18
|
+
|
|
19
|
+
__all__ = ['AegisApp', 'AegisWindow']
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Aegis Builder - AppImage Packaging System
|
|
4
|
+
|
|
5
|
+
Creates lightweight AppImages from Aegis projects.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import json
|
|
11
|
+
import shutil
|
|
12
|
+
import stat
|
|
13
|
+
import subprocess
|
|
14
|
+
import urllib.request
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AegisBuilder:
|
|
20
|
+
"""
|
|
21
|
+
Builds AppImages from Aegis projects
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
APPIMAGETOOL_URL = "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
|
25
|
+
|
|
26
|
+
def __init__(self, project_dir: str = '.'):
|
|
27
|
+
self.project_dir = Path(project_dir).resolve()
|
|
28
|
+
self.config = self._load_config()
|
|
29
|
+
|
|
30
|
+
def _load_config(self) -> dict:
|
|
31
|
+
"""Load project configuration"""
|
|
32
|
+
config_path = self.project_dir / 'aegis.config.json'
|
|
33
|
+
if config_path.exists():
|
|
34
|
+
with open(config_path) as f:
|
|
35
|
+
return json.load(f)
|
|
36
|
+
return {}
|
|
37
|
+
|
|
38
|
+
def build(self, output_dir: str = 'dist', name: Optional[str] = None) -> str:
|
|
39
|
+
"""
|
|
40
|
+
Build the AppImage
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
output_dir: Output directory for the AppImage
|
|
44
|
+
name: Custom name for the AppImage
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Path to the created AppImage
|
|
48
|
+
"""
|
|
49
|
+
app_name = name or self.config.get('name', 'AegisApp')
|
|
50
|
+
output_path = Path(output_dir).resolve()
|
|
51
|
+
appdir = output_path / f'{app_name}.AppDir'
|
|
52
|
+
|
|
53
|
+
print(f"📦 Building: {app_name}")
|
|
54
|
+
|
|
55
|
+
# Clean and create AppDir
|
|
56
|
+
if appdir.exists():
|
|
57
|
+
shutil.rmtree(appdir)
|
|
58
|
+
appdir.mkdir(parents=True)
|
|
59
|
+
|
|
60
|
+
# Create AppDir structure
|
|
61
|
+
self._create_appdir_structure(appdir, app_name)
|
|
62
|
+
|
|
63
|
+
# Copy project files
|
|
64
|
+
self._copy_project_files(appdir)
|
|
65
|
+
|
|
66
|
+
# Copy Aegis runtime
|
|
67
|
+
self._copy_aegis_runtime(appdir)
|
|
68
|
+
|
|
69
|
+
# Create AppRun script
|
|
70
|
+
self._create_apprun(appdir, app_name)
|
|
71
|
+
|
|
72
|
+
# Create .desktop file
|
|
73
|
+
self._create_desktop_file(appdir, app_name)
|
|
74
|
+
|
|
75
|
+
# Create icon
|
|
76
|
+
self._create_icon(appdir, app_name)
|
|
77
|
+
|
|
78
|
+
# Download appimagetool if needed
|
|
79
|
+
appimagetool = self._ensure_appimagetool(output_path)
|
|
80
|
+
|
|
81
|
+
# Build AppImage
|
|
82
|
+
appimage_path = output_path / f'{app_name}-x86_64.AppImage'
|
|
83
|
+
self._build_appimage(appdir, appimage_path, appimagetool)
|
|
84
|
+
|
|
85
|
+
return str(appimage_path)
|
|
86
|
+
|
|
87
|
+
def _create_appdir_structure(self, appdir: Path, app_name: str):
|
|
88
|
+
"""Create AppDir directory structure"""
|
|
89
|
+
dirs = [
|
|
90
|
+
'usr/bin',
|
|
91
|
+
'usr/lib/aegis',
|
|
92
|
+
'usr/share/applications',
|
|
93
|
+
'usr/share/icons/hicolor/256x256/apps',
|
|
94
|
+
'usr/share/aegis/runtime',
|
|
95
|
+
'app'
|
|
96
|
+
]
|
|
97
|
+
for d in dirs:
|
|
98
|
+
(appdir / d).mkdir(parents=True, exist_ok=True)
|
|
99
|
+
|
|
100
|
+
print(" 📁 Created AppDir structure")
|
|
101
|
+
|
|
102
|
+
def _copy_project_files(self, appdir: Path):
|
|
103
|
+
"""Copy project files to AppDir"""
|
|
104
|
+
app_dir = appdir / 'app'
|
|
105
|
+
|
|
106
|
+
# Copy all project files except dist, __pycache__, etc.
|
|
107
|
+
exclude = {'.git', '__pycache__', 'dist', '.venv', 'node_modules', '*.AppDir'}
|
|
108
|
+
|
|
109
|
+
for item in self.project_dir.iterdir():
|
|
110
|
+
if item.name in exclude or item.suffix == '.AppDir':
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
dest = app_dir / item.name
|
|
114
|
+
if item.is_dir():
|
|
115
|
+
shutil.copytree(item, dest, ignore=shutil.ignore_patterns(*exclude))
|
|
116
|
+
else:
|
|
117
|
+
shutil.copy2(item, dest)
|
|
118
|
+
|
|
119
|
+
print(" 📄 Copied project files")
|
|
120
|
+
|
|
121
|
+
def _copy_aegis_runtime(self, appdir: Path):
|
|
122
|
+
"""Copy Aegis runtime files"""
|
|
123
|
+
aegis_dir = Path(__file__).parent.parent
|
|
124
|
+
runtime_dir = appdir / 'usr/share/aegis'
|
|
125
|
+
|
|
126
|
+
# Create directory
|
|
127
|
+
runtime_dir.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
|
|
129
|
+
# Copy core modules
|
|
130
|
+
shutil.copytree(
|
|
131
|
+
aegis_dir / 'core',
|
|
132
|
+
runtime_dir / 'core',
|
|
133
|
+
ignore=shutil.ignore_patterns('__pycache__', '*.pyc'),
|
|
134
|
+
dirs_exist_ok=True
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Copy runtime (JS API)
|
|
138
|
+
shutil.copytree(
|
|
139
|
+
aegis_dir / 'runtime',
|
|
140
|
+
runtime_dir / 'runtime',
|
|
141
|
+
ignore=shutil.ignore_patterns('__pycache__', '*.pyc'),
|
|
142
|
+
dirs_exist_ok=True
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Create __init__.py
|
|
146
|
+
init_content = '''"""Aegis Runtime"""
|
|
147
|
+
from aegis.core.aegis import AegisApp
|
|
148
|
+
'''
|
|
149
|
+
with open(runtime_dir / '__init__.py', 'w') as f:
|
|
150
|
+
f.write(init_content)
|
|
151
|
+
|
|
152
|
+
print(" ⚡ Copied Aegis runtime")
|
|
153
|
+
|
|
154
|
+
def _create_apprun(self, appdir: Path, app_name: str):
|
|
155
|
+
"""Create AppRun script"""
|
|
156
|
+
apprun_content = '''#!/bin/bash
|
|
157
|
+
# Aegis AppRun Script
|
|
158
|
+
|
|
159
|
+
# Get the directory where AppImage is mounted
|
|
160
|
+
APPDIR="$(dirname "$(readlink -f "$0")")"
|
|
161
|
+
|
|
162
|
+
# Set up Python path
|
|
163
|
+
export PYTHONPATH="$APPDIR/usr/share:$PYTHONPATH"
|
|
164
|
+
|
|
165
|
+
# Change to app directory
|
|
166
|
+
cd "$APPDIR/app"
|
|
167
|
+
|
|
168
|
+
# Run Aegis app
|
|
169
|
+
exec python3 -c "
|
|
170
|
+
import sys
|
|
171
|
+
sys.path.insert(0, '$APPDIR/usr/share')
|
|
172
|
+
from aegis.core.aegis import AegisApp
|
|
173
|
+
app = AegisApp()
|
|
174
|
+
app.run()
|
|
175
|
+
"
|
|
176
|
+
'''
|
|
177
|
+
apprun_path = appdir / 'AppRun'
|
|
178
|
+
with open(apprun_path, 'w') as f:
|
|
179
|
+
f.write(apprun_content)
|
|
180
|
+
|
|
181
|
+
# Make executable
|
|
182
|
+
apprun_path.chmod(apprun_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
183
|
+
|
|
184
|
+
print(" 🚀 Created AppRun script")
|
|
185
|
+
|
|
186
|
+
def _create_desktop_file(self, appdir: Path, app_name: str):
|
|
187
|
+
"""Create .desktop file"""
|
|
188
|
+
desktop_content = f'''[Desktop Entry]
|
|
189
|
+
Type=Application
|
|
190
|
+
Name={self.config.get('title', app_name)}
|
|
191
|
+
Exec=AppRun
|
|
192
|
+
Icon={app_name.lower()}
|
|
193
|
+
Categories=Utility;
|
|
194
|
+
Comment={self.config.get('description', 'An Aegis application')}
|
|
195
|
+
Terminal=false
|
|
196
|
+
'''
|
|
197
|
+
# Write to AppDir root and usr/share/applications
|
|
198
|
+
for path in [appdir / f'{app_name}.desktop',
|
|
199
|
+
appdir / 'usr/share/applications' / f'{app_name}.desktop']:
|
|
200
|
+
with open(path, 'w') as f:
|
|
201
|
+
f.write(desktop_content)
|
|
202
|
+
|
|
203
|
+
print(" 📋 Created .desktop file")
|
|
204
|
+
|
|
205
|
+
def _create_icon(self, appdir: Path, app_name: str):
|
|
206
|
+
"""Create or copy app icon"""
|
|
207
|
+
icon_source = self.project_dir / self.config.get('icon', 'assets/icon.png')
|
|
208
|
+
|
|
209
|
+
if icon_source.exists():
|
|
210
|
+
# Copy existing icon
|
|
211
|
+
shutil.copy2(icon_source, appdir / f'{app_name.lower()}.png')
|
|
212
|
+
shutil.copy2(
|
|
213
|
+
icon_source,
|
|
214
|
+
appdir / 'usr/share/icons/hicolor/256x256/apps' / f'{app_name.lower()}.png'
|
|
215
|
+
)
|
|
216
|
+
print(" 🎨 Copied app icon")
|
|
217
|
+
else:
|
|
218
|
+
# Create a simple placeholder icon (1x1 transparent PNG)
|
|
219
|
+
# In production, you'd want to generate a proper icon
|
|
220
|
+
placeholder = appdir / f'{app_name.lower()}.png'
|
|
221
|
+
self._create_placeholder_icon(placeholder)
|
|
222
|
+
shutil.copy2(
|
|
223
|
+
placeholder,
|
|
224
|
+
appdir / 'usr/share/icons/hicolor/256x256/apps' / f'{app_name.lower()}.png'
|
|
225
|
+
)
|
|
226
|
+
print(" 🎨 Created placeholder icon")
|
|
227
|
+
|
|
228
|
+
def _create_placeholder_icon(self, path: Path):
|
|
229
|
+
"""Create a simple placeholder PNG icon"""
|
|
230
|
+
# Minimal valid PNG (1x1 transparent pixel)
|
|
231
|
+
# In a real scenario, we'd generate a proper icon with cairo or PIL
|
|
232
|
+
png_data = bytes([
|
|
233
|
+
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, # PNG signature
|
|
234
|
+
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, # IHDR chunk
|
|
235
|
+
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, # 1x1
|
|
236
|
+
0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4,
|
|
237
|
+
0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41,
|
|
238
|
+
0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00,
|
|
239
|
+
0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00,
|
|
240
|
+
0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE,
|
|
241
|
+
0x42, 0x60, 0x82
|
|
242
|
+
])
|
|
243
|
+
with open(path, 'wb') as f:
|
|
244
|
+
f.write(png_data)
|
|
245
|
+
|
|
246
|
+
def _ensure_appimagetool(self, output_path: Path) -> Path:
|
|
247
|
+
"""Download appimagetool if not present"""
|
|
248
|
+
tool_path = output_path / 'appimagetool'
|
|
249
|
+
|
|
250
|
+
if not tool_path.exists():
|
|
251
|
+
print(" 📥 Downloading appimagetool...")
|
|
252
|
+
urllib.request.urlretrieve(self.APPIMAGETOOL_URL, tool_path)
|
|
253
|
+
tool_path.chmod(tool_path.stat().st_mode | stat.S_IXUSR)
|
|
254
|
+
|
|
255
|
+
return tool_path
|
|
256
|
+
|
|
257
|
+
def _build_appimage(self, appdir: Path, output: Path, appimagetool: Path):
|
|
258
|
+
"""Build the final AppImage"""
|
|
259
|
+
print(" 🔨 Building AppImage...")
|
|
260
|
+
|
|
261
|
+
env = os.environ.copy()
|
|
262
|
+
env['ARCH'] = 'x86_64'
|
|
263
|
+
|
|
264
|
+
result = subprocess.run(
|
|
265
|
+
[str(appimagetool), str(appdir), str(output)],
|
|
266
|
+
env=env,
|
|
267
|
+
capture_output=True,
|
|
268
|
+
text=True
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if result.returncode != 0:
|
|
272
|
+
print(f" ❌ appimagetool error: {result.stderr}")
|
|
273
|
+
raise RuntimeError(f"appimagetool failed: {result.stderr}")
|
|
274
|
+
|
|
275
|
+
# Clean up AppDir
|
|
276
|
+
shutil.rmtree(appdir)
|
|
277
|
+
|
|
278
|
+
print(f" ✅ AppImage created: {output.name}")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def build_project(project_dir: str = '.', output_dir: str = 'dist', name: str = None) -> str:
|
|
282
|
+
"""
|
|
283
|
+
Convenience function to build a project
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
project_dir: Path to the project
|
|
287
|
+
output_dir: Output directory
|
|
288
|
+
name: Custom AppImage name
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Path to the created AppImage
|
|
292
|
+
"""
|
|
293
|
+
builder = AegisBuilder(project_dir)
|
|
294
|
+
return builder.build(output_dir, name)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
if __name__ == '__main__':
|
|
298
|
+
if len(sys.argv) > 1:
|
|
299
|
+
build_project(sys.argv[1])
|
|
300
|
+
else:
|
|
301
|
+
build_project()
|
|
Binary file
|
|
Binary file
|