omarchy-ui 0.0.8-x86_64-linux → 0.0.9-x86_64-linux

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bbe75336899d8f9674166011c929469db798869f88b32f45a3b61505b0713d01
4
- data.tar.gz: bb5f2e4f0dfec4578992d17d5f3fdc5530d5b2c73d4aeebcfada553355077f94
3
+ metadata.gz: 82c12f3f51c0d9242be7386a4994c4f3fe8b333d04489e4055b671054cb2f0e2
4
+ data.tar.gz: e693d5b68a5ad055b40fbb910b4bb6ef41b6de7405027396305f256ad744875d
5
5
  SHA512:
6
- metadata.gz: 9776119537e37548ea84ca4129647a6f94b64944ae75315e776db661220aefeb4cc80527ed93a1b1c9410e0b80d2a547c77a2aae6674f770decc354bdee2a505
7
- data.tar.gz: 6856581d58533845967d37daae2679cbf9601f9555aa724b9a9cbf04da0a62a8b15cd146df6d3b36551dec85ad29a13189ba1bdb01a143c2a3cfed87f46d2bb4
6
+ metadata.gz: 807cb5e30c33130681351065bc7fce902a7c4c125f1c6b47e8827db0ec6b49695745ab60d61da9475153f8abf1422940df280154af797ede39ef218a9097c449
7
+ data.tar.gz: c2f3532f53a5725e7c6e7cd9de667ebbb2dddd086204af8172f8b262c010e4fef2470b73716a7496c9859d396f43b22814e03926949944e2e1c080e1e64ca5a2
data/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  </p>
11
11
 
12
12
  <p align="center">
13
- <img alt="Omarchy UI 0.0.8" src="https://img.shields.io/badge/Omarchy_UI-0.0.8-b7ff5a?style=flat-square&labelColor=111711">
13
+ <img alt="Omarchy UI 0.0.9" src="https://img.shields.io/badge/Omarchy_UI-0.0.9-b7ff5a?style=flat-square&labelColor=111711">
14
14
  <img alt="Ruby 3.1 or newer" src="https://img.shields.io/badge/Ruby-3.1%2B-cc342d?style=flat-square&logo=ruby&logoColor=white">
15
15
  <img alt="Zui 0.0.10" src="https://img.shields.io/badge/Zui-0.0.10-7ee14b?style=flat-square&labelColor=111711">
16
16
  <img alt="241 components" src="https://img.shields.io/badge/components-241-75d943?style=flat-square&labelColor=111711">
@@ -46,8 +46,8 @@ and the Omarchy adapter.
46
46
  </table>
47
47
 
48
48
  > [!IMPORTANT]
49
- > Omarchy UI currently targets **Omarchy on x86-64 Linux** and is marked experimental. The reusable
50
- > Zui application module can also run with Zui's native host on Linux, macOS, and Windows.
49
+ > Omarchy UI currently targets **Omarchy on x86-64 Linux**. Reusable Zui application modules can
50
+ > also run through Zui's native host on Linux, macOS, and Windows.
51
51
 
52
52
  ## Choose a workflow
53
53
 
@@ -59,15 +59,16 @@ and the Omarchy adapter.
59
59
 
60
60
  ## Requirements
61
61
 
62
- - Omarchy on x86-64 Linux;
63
- - Ruby 3.1 or newer while installing the gem and developing applications;
64
- - optional Qt modules for components that use features such as Quick 3D or Multimedia.
62
+ - Omarchy on x86-64 Linux
63
+ - Ruby 3.1 or newer for installation and development
64
+ - Qt 6 development tools, CMake, Ninja, and a C++ compiler for `bundle`, `validate`, and `push`
65
+ - Optional Qt modules for components that use features such as Quick 3D or Multimedia
65
66
 
66
- Omarchy UI generates a complete Omarchy-compatible application or plugin package from the
67
- developer's Ruby and assets. It owns the Omarchy metadata, entry points, adapter, validation,
68
- tree-shaken Zui QML, and embedded mruby runtime. The finished package does not need Ruby or framework
69
- gems. A component fails with an explicit error when its required Qt module or resource is unavailable;
70
- Omarchy UI never substitutes a different component.
67
+ Omarchy UI turns Ruby and assets into an Omarchy-compatible application or plugin. It owns the
68
+ metadata, entry points, adapter, validation, tree-shaken and AOT-compiled Zui QML, and embedded mruby
69
+ runtime. Omarchy UI invokes the build tools; projects do not contain CMake files or distribution
70
+ scripts. The finished package does not require Ruby or framework gems. Components report a clear
71
+ error when a required Qt module or resource is unavailable.
71
72
 
72
73
  ## Quick start
73
74
 
@@ -79,7 +80,7 @@ gem install omarchy-ui
79
80
 
80
81
  The gem installs the compatible Zui version as a dependency.
81
82
 
82
- ### 2. Generate a complete project
83
+ ### 2. Generate a project
83
84
 
84
85
  ```bash
85
86
  omarchy_ui new Counter
@@ -93,13 +94,12 @@ omarchy_ui new "System Status" --plugin
93
94
  cd system-status
94
95
  ```
95
96
 
96
- Both commands create a ready-to-run Ruby project. Omarchy UI generates the complete compliant
97
- application or plugin package; developers do not write QML, shell entry points, or an Omarchy
98
- manifest by hand.
97
+ Both commands create a ready-to-run Ruby project. Developers write the interface and behavior;
98
+ Omarchy UI generates the Omarchy package without application-owned QML, shell entry points, or
99
+ handwritten manifests.
99
100
 
100
- Each project includes a Ruby-native `config.rb` containing its type, identity, release version,
101
- entry point, and—for plugins—public marketplace metadata. `bundle`, `validate`, and `push` read this
102
- single configuration when assembling the complete package.
101
+ Each project includes a Ruby-native `config.rb` with its type, identity, version, entry point, and
102
+ plugin metadata. `bundle`, `validate`, and `push` use this single configuration.
103
103
 
104
104
  The generator creates a small, host-independent project:
105
105
 
@@ -120,8 +120,8 @@ counter/
120
120
  omarchy_ui run main.rb
121
121
  ```
122
122
 
123
- `run` bundles the project's Ruby entry point, asks Zui to tree-shake the QML runtime, prepares the Omarchy host,
124
- then launches it through Quickshell.
123
+ `run` bundles the project's Ruby entry point, asks Zui to tree-shake the QML runtime, prepares the
124
+ Omarchy host, and launches it through Quickshell.
125
125
 
126
126
  ## Application model
127
127
 
@@ -246,16 +246,15 @@ QML is not required.
246
246
 
247
247
  ## Omarchy plugins
248
248
 
249
- Generate a complete plugin project with one command:
249
+ Generate a plugin project with one command:
250
250
 
251
251
  ```bash
252
252
  omarchy_ui new "System Status" --plugin
253
253
  cd system-status
254
254
  ```
255
255
 
256
- The generated project already satisfies Omarchy's structural requirements. Developers work in Ruby
257
- and add their own assets; Omarchy UI owns the required plugin metadata, shell surfaces, native host,
258
- tree-shaken Zui package, validation, and install lifecycle.
256
+ Developers work in Ruby and add their own assets. Omarchy UI supplies the plugin metadata, shell
257
+ surfaces, native host, tree-shaken Zui package, validation, and installation lifecycle.
259
258
 
260
259
  The Ruby entry point uses ordinary Zui components:
261
260
 
@@ -282,8 +281,8 @@ OmarchyUI.plugin do
282
281
  end
283
282
  ```
284
283
 
285
- `bar_widget` and `panel :status` are the complete developer-facing surface declarations. Omarchy UI
286
- maps them to the required Omarchy shell entry points when it builds or stages the plugin.
284
+ `bar_widget` and `panel :status` are the developer-facing surface declarations. Omarchy UI maps them
285
+ to the required shell entry points when it builds or stages the plugin.
287
286
 
288
287
  ### Validate and install
289
288
 
@@ -292,8 +291,8 @@ omarchy_ui validate
292
291
  omarchy_ui push
293
292
  ```
294
293
 
295
- `validate` generates and checks the complete plugin without mutating the Ruby project. `push` safely
296
- builds, validates, installs, enables, and reloads the plugin for development.
294
+ `validate` generates and checks the plugin without modifying the Ruby project. `push` builds,
295
+ validates, installs, enables, and reloads it for development.
297
296
 
298
297
  During development, suppress either lifecycle action when needed:
299
298
 
@@ -302,10 +301,6 @@ omarchy_ui push --no-enable
302
301
  omarchy_ui push --no-restart
303
302
  ```
304
303
 
305
- > [!WARNING]
306
- > Omarchy plugins execute as unsandboxed user code inside the shell environment. Review the Ruby,
307
- > assets, generated package, and provenance before installing a third-party plugin.
308
-
309
304
  ## Command-line reference
310
305
 
311
306
  ```text
@@ -314,11 +309,11 @@ omarchy_ui <command> [arguments]
314
309
 
315
310
  | Command | Arguments | Result |
316
311
  | --- | --- | --- |
317
- | `omarchy_ui new NAME` | One project name | Creates a complete Omarchy application project |
318
- | `omarchy_ui new NAME --plugin` | One project name and `--plugin` | Creates a complete Omarchy-compliant plugin project |
312
+ | `omarchy_ui new NAME` | One project name | Creates an Omarchy application project |
313
+ | `omarchy_ui new NAME --plugin` | One project name and `--plugin` | Creates an Omarchy plugin project |
319
314
  | `omarchy_ui run FILE` | One Ruby entry point | Runs an application through the Omarchy/Quickshell host |
320
- | `omarchy_ui bundle [DIRECTORY]` | Zero or one project directory | Generates the complete distributable package and prints its location |
321
- | `omarchy_ui validate [DIRECTORY]` | Zero or one plugin directory | Stages the complete plugin and delegates validation to `omarchy plugin validate` |
315
+ | `omarchy_ui bundle [DIRECTORY]` | Zero or one project directory | Generates the distributable package and prints its location |
316
+ | `omarchy_ui validate [DIRECTORY]` | Zero or one plugin directory | Stages the plugin and delegates validation to `omarchy plugin validate` |
322
317
  | `omarchy_ui push [DIRECTORY]` | Optional `--no-enable` and `--no-restart` | Atomically installs and activates a validated plugin |
323
318
  | `omarchy_ui version` | None | Prints the installed Omarchy UI version |
324
319
 
@@ -327,19 +322,29 @@ not forward arbitrary Ruby arguments.
327
322
 
328
323
  ## Packaging
329
324
 
330
- Create the complete distributable package from the project root:
325
+ Create the distributable package from the project root:
331
326
 
332
327
  ```bash
333
328
  omarchy_ui bundle
334
329
  ```
335
330
 
336
- Omarchy UI prints the generated package location. It automatically assembles the bundled Ruby and
337
- assets, Omarchy metadata and entry points, native host, checksummed runtime provenance, and only the
338
- Zui QML components used by the project. Application packages receive a direct launcher; plugin
339
- packages are validated against Omarchy's requirements before the command succeeds. Omarchy UI pins
340
- Zui 0.0.10 so generated QML exactly matches the Zui core embedded in the deterministic mruby runtime.
341
- The source `config.rb` remains the authoritative build configuration; plugin manifests are generated
342
- from it in the distributable package and are never maintained separately by the developer.
331
+ Omarchy UI assembles the Ruby source and assets, Omarchy entry points, native host, checksummed
332
+ runtime, and only the Zui components used by the project. It then AOT-compiles the generated QML into
333
+ a content-addressed Qt module and discards the generated component, control, theme, and font source
334
+ tree. Applications receive a direct launcher; plugins must pass Omarchy validation before the
335
+ command succeeds. Omarchy UI pins Zui 0.0.10 so the compiled UI matches the Zui core embedded in the
336
+ deterministic mruby runtime.
337
+
338
+ Current Omarchy manifests resolve entry points by filename, so a package retains four minimal loader
339
+ files: `App.qml`, `Service.qml`, `Panel.qml`, and `BarWidget.qml`. They contain only an import and a
340
+ compiled type instance; the application interface is stored in native `.so` artifacts. The package
341
+ also includes `omarchy-ui-qml-bundle.json` with the module URI and exact Qt version, plus
342
+ `omarchy-ui-qml-bundle.sha256` for artifact verification. `QML_PROVENANCE.md` explains the native
343
+ artifacts for package reviewers. Compiled packages should be built against the Qt version shipped by
344
+ their target Omarchy release.
345
+
346
+ The source `config.rb` is the build configuration. Omarchy UI generates plugin manifests from it;
347
+ developers never maintain a separate manifest.
343
348
 
344
349
  ## Component catalog
345
350
 
@@ -406,10 +411,12 @@ Zui DSL · state · events · bindings · scheduler
406
411
 
407
412
  │ versioned JSON render, patch, event, and effect protocol
408
413
 
409
- Omarchy Service.qml process bridge ───── embedded mruby runtime
414
+ Omarchy process bridge ───────────────── embedded mruby runtime
410
415
 
411
416
 
412
- Zui ControlNode + native component catalog
417
+ Tree-shaken Zui host + component catalog
418
+
419
+ │ bundle: Qt AOT module + four loader shims
413
420
 
414
421
 
415
422
  App window · Omarchy panel · Omarchy bar widget
@@ -427,8 +434,9 @@ The dependency direction is deliberate:
427
434
  - **Applications own** Ruby source, assets, state, behavior, and reusable UI modules.
428
435
 
429
436
  At bundle, validation, and push time, Omarchy UI asks Zui to analyze the bundled Ruby syntax and
430
- generate only the required component adapters plus their renderer dependencies. The four Omarchy
431
- host files then replace Zui's generic desktop host. Read the full [QML ownership and adapter
437
+ generate only the required component adapters plus their renderer dependencies. Omarchy UI overlays
438
+ its host, compiles the complete generated QML graph into a content-addressed Qt module, and emits the
439
+ minimal loader files required by Omarchy. Read the full [QML ownership and adapter
432
440
  boundary](docs/qml-support.md).
433
441
 
434
442
  ## Runtime integrity
@@ -114,14 +114,17 @@ module OmarchyUI
114
114
  config = ProjectConfig.load(source)
115
115
  entrypoint = config&.entrypoint || "main.rb"
116
116
  raise ArgumentError, "#{entrypoint} not found: #{source}" unless File.file?(File.join(source, entrypoint))
117
+ precompiled = config.nil? && compiled_package?(source)
117
118
  destination = File.join(source, "dist", config&.slug || File.basename(source))
118
119
  raise ArgumentError, "bundle destination already exists: #{destination}" if File.exist?(destination)
119
120
  FileUtils.mkdir_p(destination)
120
- entries = application_entries(source, config:)
121
+ entries = precompiled ? package_entries(source) : application_entries(source, config:)
121
122
  FileUtils.cp_r(entries.map { |entry| File.join(source, entry) }, destination)
122
- config.write_manifest(destination) if config&.plugin?
123
- bundle_ruby_entrypoint(source, destination, entrypoint:)
124
- Runtime.install_package(destination)
123
+ unless precompiled
124
+ config.write_manifest(destination) if config&.plugin?
125
+ bundle_ruby_entrypoint(source, destination, entrypoint:)
126
+ Runtime.install_package(destination, compiled: !config.nil?)
127
+ end
125
128
  plugin = config ? config.plugin? : File.file?(File.join(destination, "manifest.json"))
126
129
  if plugin
127
130
  raise ArgumentError, "bundled plugin validation failed" unless system("omarchy", "plugin", "validate", destination)
@@ -140,6 +143,8 @@ module OmarchyUI
140
143
  export OMARCHY_UI_RUNTIME="$app_dir/omarchy-ui-runtime"
141
144
  export OMARCHY_UI_PROJECT_DIR="$app_dir"
142
145
  export OMARCHY_UI_RUBY_PROGRAM="$app_dir/#{entrypoint}"
146
+ export QML2_IMPORT_PATH="$app_dir${QML2_IMPORT_PATH:+:$QML2_IMPORT_PATH}"
147
+ export QML_IMPORT_PATH="$app_dir${QML_IMPORT_PATH:+:$QML_IMPORT_PATH}"
143
148
  export QT_LOGGING_RULES="${QT_LOGGING_RULES:+$QT_LOGGING_RULES;}qt.qpa.services.warning=false"
144
149
  exec quickshell --path "$app_dir/App.qml"
145
150
  SH
@@ -161,6 +166,18 @@ module OmarchyUI
161
166
  Dir.children(source).reject { |entry| %w[.git dist].include?(entry) || generated.include?(entry) }
162
167
  end
163
168
 
169
+ def package_entries(source)
170
+ Dir.children(source).reject { |entry| %w[.git dist].include?(entry) }
171
+ end
172
+
173
+ def compiled_package?(source)
174
+ report = File.join(source, QmlCompiler::REPORT)
175
+ return false unless File.file?(report)
176
+
177
+ QmlCompiler.verify!(source)
178
+ true
179
+ end
180
+
164
181
  def push(arguments)
165
182
  enable = !arguments.delete("--no-enable")
166
183
  restart = !arguments.delete("--no-restart")
@@ -213,14 +230,17 @@ module OmarchyUI
213
230
  def stage_project(source, parent: nil, prefix: ".omarchy-ui-staging-")
214
231
  raise ArgumentError, "project directory not found: #{source}" unless File.directory?(source)
215
232
  config = ProjectConfig.load(source)
233
+ precompiled = config.nil? && compiled_package?(source)
216
234
  staging = parent ? Dir.mktmpdir(prefix, parent) : Dir.mktmpdir(prefix)
217
- entries = application_entries(source, config:)
235
+ entries = precompiled ? package_entries(source) : application_entries(source, config:)
218
236
  FileUtils.cp_r(entries.map { |entry| File.join(source, entry) }, staging) unless entries.empty?
219
- config.write_manifest(staging) if config&.plugin?
220
- entrypoint = config&.entrypoint || "main.rb"
221
- if File.file?(File.join(staging, entrypoint))
222
- bundle_ruby_entrypoint(source, staging, entrypoint:)
223
- Runtime.install_package(staging)
237
+ unless precompiled
238
+ config.write_manifest(staging) if config&.plugin?
239
+ entrypoint = config&.entrypoint || "main.rb"
240
+ if File.file?(File.join(staging, entrypoint))
241
+ bundle_ruby_entrypoint(source, staging, entrypoint:)
242
+ Runtime.install_package(staging, compiled: !config.nil?)
243
+ end
224
244
  end
225
245
  staging
226
246
  rescue StandardError
@@ -139,7 +139,8 @@ module OmarchyUI
139
139
  omarchy_ui bundle
140
140
  ```
141
141
 
142
- Omarchy UI generates the complete self-contained application package required by Omarchy.
142
+ Omarchy UI tree-shakes and compiles the UI, then generates the complete self-contained
143
+ application package required by Omarchy.
143
144
  MARKDOWN
144
145
  end
145
146
 
@@ -168,8 +169,8 @@ module OmarchyUI
168
169
  ```
169
170
 
170
171
  Write the plugin interface and behavior in Ruby under `main.rb` and `components/`.
171
- Public plugin identity and release metadata live in `config.rb`. Omarchy UI uses it to
172
- generate and validate the complete Omarchy-compatible plugin package.
172
+ Public plugin identity and release metadata live in `config.rb`. Omarchy UI tree-shakes and
173
+ compiles the UI, then generates and validates the complete Omarchy-compatible plugin package.
173
174
  MARKDOWN
174
175
  end
175
176
 
@@ -115,6 +115,7 @@ module OmarchyUI
115
115
  value = @data[:type]
116
116
  value.respond_to?(:to_sym) ? value.to_sym : value
117
117
  end
118
+
118
119
  def application? = type == :application
119
120
  def plugin? = type == :plugin
120
121
  def name = @data.fetch(:name)
@@ -0,0 +1,386 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "fileutils"
5
+ require "json"
6
+ require "open3"
7
+ require "pathname"
8
+ require "tmpdir"
9
+
10
+ module OmarchyUI
11
+ class QmlCompiler
12
+ FORMAT_VERSION = 1
13
+ ENTRY_TYPES = {
14
+ "App.qml" => "App",
15
+ "BarWidget.qml" => "BarWidget",
16
+ "Panel.qml" => "Panel",
17
+ "Service.qml" => "Service"
18
+ }.freeze
19
+ SOURCE_ENTRIES = (ENTRY_TYPES.keys + %w[ControlNode.qml Components Controls Theme Fonts]).freeze
20
+ COMPILED_ROOT = "OmarchyUI"
21
+ REPORT = "omarchy-ui-qml-bundle.json"
22
+ CHECKSUM = "omarchy-ui-qml-bundle.sha256"
23
+ PROVENANCE = "QML_PROVENANCE.md"
24
+
25
+ def self.compile!(path)
26
+ new(path).compile!
27
+ end
28
+
29
+ def self.verify!(path)
30
+ new(path).verify!
31
+ end
32
+
33
+ def initialize(path)
34
+ @path = File.expand_path(path)
35
+ end
36
+
37
+ def compile!
38
+ validate_sources!
39
+ tools = build_tools
40
+ qt_version = qt_version!(tools.fetch(:qtpaths))
41
+
42
+ Dir.mktmpdir("omarchy-ui-qml-compile-") do |temporary|
43
+ source = File.join(temporary, "source")
44
+ build = File.join(temporary, "build")
45
+ stage_sources(source)
46
+ qml_files = relative_files(source, "**/*.qml")
47
+ resources = relative_files(source, "Fonts/**/*") +
48
+ %w[Controls/qmldir Theme/qmldir].select { |entry| File.file?(File.join(source, entry)) }
49
+ fingerprint = source_fingerprint(source, qml_files + resources, qt_version:)
50
+ segment = "B#{fingerprint[0, 20]}"
51
+ uri = "OmarchyUI.Bundles.#{segment}"
52
+ target = "omarchy_ui_bundle_#{segment.downcase}"
53
+ module_path = File.join(*uri.split("."))
54
+
55
+ File.write(File.join(source, "CMakeLists.txt"), cmake_project(
56
+ qml_files:,
57
+ resources: resources.sort,
58
+ uri:,
59
+ target:,
60
+ module_path:
61
+ ))
62
+ configure!(tools.fetch(:qt_cmake), source, build)
63
+ build!(tools.fetch(:cmake), build)
64
+ deploy!(
65
+ build:,
66
+ module_path:,
67
+ target:,
68
+ uri:,
69
+ fingerprint:,
70
+ qml_files:,
71
+ resources:,
72
+ source:,
73
+ expected_qt_version: qt_version
74
+ )
75
+ end
76
+ end
77
+
78
+ def verify!
79
+ report_path = File.join(@path, REPORT)
80
+ raise ArgumentError, "compiled QML report is missing: #{report_path}" unless File.file?(report_path)
81
+
82
+ report = JSON.parse(File.read(report_path))
83
+ unless report["format"] == "qt-aot-qml-module" && report["format_version"] == FORMAT_VERSION
84
+ raise ArgumentError, "unsupported compiled QML package format"
85
+ end
86
+ ENTRY_TYPES.each_key do |entry|
87
+ raise ArgumentError, "compiled QML entry shim is missing: #{entry}" unless File.file?(File.join(@path, entry))
88
+ end
89
+ module_path = safe_relative_path(report.fetch("module_path"))
90
+ raise ArgumentError, "compiled QML qmldir is missing" unless File.file?(File.join(@path, module_path, "qmldir"))
91
+
92
+ artifacts = report.fetch("artifacts")
93
+ raise ArgumentError, "compiled QML package must contain two libraries" unless artifacts.length == 2
94
+ artifacts.each do |artifact|
95
+ relative = safe_relative_path(artifact.fetch("path"))
96
+ unless relative.start_with?("#{module_path}/") && relative.end_with?(".so")
97
+ raise ArgumentError, "compiled QML artifact is outside its module: #{relative}"
98
+ end
99
+ absolute = File.join(@path, relative)
100
+ raise ArgumentError, "compiled QML artifact is missing: #{relative}" unless File.file?(absolute)
101
+ unless File.size(absolute) == artifact.fetch("bytes") &&
102
+ Digest::SHA256.file(absolute).hexdigest == artifact.fetch("sha256")
103
+ raise ArgumentError, "compiled QML artifact checksum mismatch: #{relative}"
104
+ end
105
+ end
106
+ report
107
+ rescue JSON::ParserError, KeyError, TypeError => error
108
+ raise ArgumentError, "invalid compiled QML report: #{error.message}"
109
+ end
110
+
111
+ private
112
+
113
+ def safe_relative_path(path)
114
+ value = path.to_s
115
+ clean = File.expand_path(value, @path)
116
+ unless !value.empty? && !Pathname.new(value).absolute? && clean.start_with?("#{@path}/")
117
+ raise ArgumentError, "unsafe compiled QML path: #{value.inspect}"
118
+ end
119
+ clean.delete_prefix("#{@path}/")
120
+ end
121
+
122
+ def validate_sources!
123
+ missing = ENTRY_TYPES.keys.reject { |entry| File.file?(File.join(@path, entry)) }
124
+ missing << "ControlNode.qml" unless File.file?(File.join(@path, "ControlNode.qml"))
125
+ return if missing.empty?
126
+
127
+ raise ArgumentError, "cannot compile QML; generated files are missing: #{missing.join(', ')}"
128
+ end
129
+
130
+ def build_tools
131
+ {
132
+ qt_cmake: find_executable("/usr/lib/qt6/bin/qt-cmake", "qt-cmake"),
133
+ qtpaths: find_executable("/usr/lib/qt6/bin/qtpaths", "qtpaths6", "qtpaths"),
134
+ cmake: find_executable("cmake"),
135
+ ninja: find_executable("ninja"),
136
+ compiler: find_executable("c++", "g++", "clang++")
137
+ }
138
+ end
139
+
140
+ def qt_version!(qtpaths)
141
+ output, error, status = Open3.capture3(qtpaths, "--query", "QT_VERSION")
142
+ version = output.strip
143
+ return version if status.success? && !version.empty?
144
+
145
+ detail = error.strip
146
+ raise ArgumentError, "could not determine the Qt build version#{detail.empty? ? '' : ": #{detail}"}"
147
+ end
148
+
149
+ def find_executable(*candidates)
150
+ candidates.each do |candidate|
151
+ if candidate.include?(File::SEPARATOR)
152
+ return candidate if File.file?(candidate) && File.executable?(candidate)
153
+ next
154
+ end
155
+
156
+ ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |directory|
157
+ path = File.join(directory, candidate)
158
+ return path if File.file?(path) && File.executable?(path)
159
+ end
160
+ end
161
+ raise ArgumentError, "compiled QML packaging requires #{candidates.first}"
162
+ end
163
+
164
+ def stage_sources(destination)
165
+ FileUtils.mkdir_p(destination)
166
+ SOURCE_ENTRIES.each do |entry|
167
+ source = File.join(@path, entry)
168
+ next unless File.exist?(source)
169
+
170
+ FileUtils.cp_r(source, File.join(destination, entry))
171
+ end
172
+ end
173
+
174
+ def relative_files(root, pattern)
175
+ Dir.glob(File.join(root, pattern), File::FNM_DOTMATCH)
176
+ .select { |path| File.file?(path) }
177
+ .map { |path| path.delete_prefix("#{root}/") }
178
+ .sort
179
+ end
180
+
181
+ def source_fingerprint(root, files, qt_version:)
182
+ digest = Digest::SHA256.new
183
+ digest << "omarchy-ui-qml-bundle\0" << FORMAT_VERSION.to_s << "\0"
184
+ digest << "qt\0" << qt_version << "\0"
185
+ files.sort.each do |relative|
186
+ digest << relative << "\0" << File.binread(File.join(root, relative)) << "\0"
187
+ end
188
+ digest.hexdigest
189
+ end
190
+
191
+ def cmake_project(qml_files:, resources:, uri:, target:, module_path:)
192
+ <<~CMAKE
193
+ cmake_minimum_required(VERSION 3.21)
194
+
195
+ project(OmarchyUICompiledBundle LANGUAGES CXX)
196
+
197
+ find_package(Qt6 6.8 REQUIRED COMPONENTS Core Qml Quick QuickControls2)
198
+ qt_standard_project_setup(REQUIRES 6.8)
199
+
200
+ qt_add_library(#{target} SHARED)
201
+ qt_add_qml_module(#{target}
202
+ URI #{uri}
203
+ VERSION 1.0
204
+ OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/#{module_path}"
205
+ QML_FILES
206
+ #{cmake_entries(qml_files)}
207
+ RESOURCES
208
+ #{cmake_entries(resources)}
209
+ DISCARD_QML_CONTENTS
210
+ NO_LINT
211
+ )
212
+
213
+ set(module_output "${CMAKE_BINARY_DIR}/#{module_path}")
214
+ set_target_properties(
215
+ #{target}
216
+ #{target}plugin
217
+ PROPERTIES
218
+ LIBRARY_OUTPUT_DIRECTORY "${module_output}"
219
+ BUILD_RPATH "\\$ORIGIN"
220
+ BUILD_WITH_INSTALL_RPATH TRUE
221
+ INSTALL_RPATH "\\$ORIGIN"
222
+ )
223
+
224
+ target_compile_options(
225
+ #{target}
226
+ PRIVATE
227
+ "-ffile-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=."
228
+ "-ffile-prefix-map=${CMAKE_CURRENT_BINARY_DIR}=."
229
+ )
230
+ target_compile_options(
231
+ #{target}plugin
232
+ PRIVATE
233
+ "-ffile-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=."
234
+ "-ffile-prefix-map=${CMAKE_CURRENT_BINARY_DIR}=."
235
+ )
236
+ target_link_options(#{target} PRIVATE "-Wl,--build-id=none")
237
+ target_link_options(#{target}plugin PRIVATE "-Wl,--build-id=none")
238
+ target_link_libraries(
239
+ #{target}
240
+ PRIVATE Qt6::Core Qt6::Qml Qt6::Quick Qt6::QuickControls2
241
+ )
242
+
243
+ file(WRITE "${CMAKE_BINARY_DIR}/qt-version.txt" "${Qt6Core_VERSION}\n")
244
+ CMAKE
245
+ end
246
+
247
+ def cmake_entries(entries)
248
+ entries.map { |entry| " #{cmake_string(entry)}" }.join("\n")
249
+ end
250
+
251
+ def cmake_string(value)
252
+ escaped = value.gsub("\\", "\\\\").gsub('"', '\\"')
253
+ %Q("#{escaped}")
254
+ end
255
+
256
+ def configure!(qt_cmake, source, build)
257
+ run_build_command!(
258
+ "QML configure",
259
+ qt_cmake,
260
+ "-S", source,
261
+ "-B", build,
262
+ "-G", "Ninja",
263
+ "-DCMAKE_BUILD_TYPE=Release"
264
+ )
265
+ end
266
+
267
+ def build!(cmake, build)
268
+ run_build_command!("QML compilation", cmake, "--build", build, "--parallel")
269
+ end
270
+
271
+ def run_build_command!(label, *command)
272
+ environment = {
273
+ "LC_ALL" => "C.UTF-8",
274
+ "SOURCE_DATE_EPOCH" => "1",
275
+ "TZ" => "UTC"
276
+ }
277
+ output, error, status = Open3.capture3(environment, *command)
278
+ return if status.success?
279
+
280
+ detail = "#{output}\n#{error}".lines.last(40).join.strip
281
+ raise ArgumentError, "#{label} failed#{detail.empty? ? '' : ":\n#{detail}"}"
282
+ end
283
+
284
+ def deploy!(build:, module_path:, target:, uri:, fingerprint:, qml_files:, resources:, source:,
285
+ expected_qt_version:)
286
+ built_module = File.join(build, module_path)
287
+ artifact_names = ["lib#{target}.so", "lib#{target}plugin.so", "qmldir"]
288
+ missing = artifact_names.reject { |name| File.file?(File.join(built_module, name)) }
289
+ unless missing.empty?
290
+ raise ArgumentError, "QML compilation did not produce: #{missing.join(', ')}"
291
+ end
292
+ qt_version = File.read(File.join(build, "qt-version.txt")).strip
293
+ unless qt_version == expected_qt_version
294
+ raise ArgumentError,
295
+ "Qt compiler mismatch: qtpaths reported #{expected_qt_version}, CMake found #{qt_version}"
296
+ end
297
+
298
+ compiled_root = File.join(@path, COMPILED_ROOT)
299
+ FileUtils.remove_entry(compiled_root) if File.directory?(compiled_root)
300
+ destination = File.join(@path, module_path)
301
+ FileUtils.mkdir_p(destination)
302
+ artifact_names.each { |name| FileUtils.cp(File.join(built_module, name), destination) }
303
+ qmldir = File.join(destination, "qmldir")
304
+ File.write(qmldir, File.read(qmldir).rstrip + "\n")
305
+
306
+ SOURCE_ENTRIES.each { |entry| remove_generated(File.join(@path, entry)) }
307
+ ENTRY_TYPES.each do |entry, type|
308
+ File.write(File.join(@path, entry), entry_shim(module_path, type, uri:))
309
+ end
310
+
311
+ artifact_paths = artifact_names.filter_map do |name|
312
+ next if name == "qmldir"
313
+
314
+ relative = File.join(module_path, name)
315
+ absolute = File.join(@path, relative)
316
+ {
317
+ "path" => relative,
318
+ "sha256" => Digest::SHA256.file(absolute).hexdigest,
319
+ "bytes" => File.size(absolute)
320
+ }
321
+ end
322
+ report = {
323
+ "format" => "qt-aot-qml-module",
324
+ "format_version" => FORMAT_VERSION,
325
+ "qt_version" => qt_version,
326
+ "module_uri" => uri,
327
+ "module_path" => module_path,
328
+ "source_fingerprint" => fingerprint,
329
+ "source_files" => qml_files.length,
330
+ "source_bytes" => (qml_files + resources).sum { |file| File.size(File.join(source, file)) },
331
+ "entry_shims" => ENTRY_TYPES.keys,
332
+ "artifacts" => artifact_paths
333
+ }
334
+ File.write(File.join(@path, REPORT), JSON.pretty_generate(report) + "\n")
335
+ checksums = artifact_paths.map { |artifact| "#{artifact.fetch('sha256')} #{artifact.fetch('path')}" }
336
+ File.write(File.join(@path, CHECKSUM), checksums.join("\n") + "\n")
337
+ File.write(File.join(@path, PROVENANCE), provenance(report))
338
+ report
339
+ end
340
+
341
+ def remove_generated(path)
342
+ File.directory?(path) ? FileUtils.remove_entry(path) : FileUtils.rm_f(path)
343
+ end
344
+
345
+ def entry_shim(module_path, type, uri:)
346
+ import = type == "App" ? uri : %Q("#{module_path}")
347
+ <<~QML
348
+ import QtQuick
349
+ import #{import} as Compiled
350
+
351
+ Compiled.#{type} {}
352
+ QML
353
+ end
354
+
355
+ def provenance(report)
356
+ artifacts = report.fetch("artifacts").map do |artifact|
357
+ "- `#{artifact.fetch('path')}` — `#{artifact.fetch('sha256')}`"
358
+ end.join("\n")
359
+ <<~MARKDOWN
360
+ # Compiled QML provenance
361
+
362
+ Omarchy UI generated this package's native Qt module from the tree-shaken Zui and
363
+ Omarchy host QML graph. Generated QML source contents were discarded after AOT compilation.
364
+
365
+ - Format: `#{report.fetch('format')}` version #{report.fetch('format_version')}
366
+ - Qt: `#{report.fetch('qt_version')}`
367
+ - Module: `#{report.fetch('module_uri')}`
368
+ - Source fingerprint: `#{report.fetch('source_fingerprint')}`
369
+
370
+ ## Artifacts
371
+
372
+ #{artifacts}
373
+
374
+ Verify the packaged libraries from the plugin directory:
375
+
376
+ ```bash
377
+ sha256sum --check #{CHECKSUM}
378
+ ```
379
+
380
+ `App.qml`, `Service.qml`, `Panel.qml`, and `BarWidget.qml` are the minimal loader shims
381
+ required by Omarchy's file-based entry-point contract. Application UI lives in the compiled
382
+ module recorded by `#{REPORT}`.
383
+ MARKDOWN
384
+ end
385
+ end
386
+ end
@@ -16,8 +16,14 @@ module OmarchyUI
16
16
  AUDIT_FILES = %w[
17
17
  omarchy-ui-runtime.sha256 runtime-provenance.json RUNTIME_PROVENANCE.md
18
18
  ].freeze
19
- GENERATED_ENTRIES = (ADAPTER_FILES + ZUI_GENERATED_ENTRIES +
20
- [TREE_SHAKE_REPORT, "omarchy-ui-runtime"]).freeze
19
+ GENERATED_ENTRIES = (ADAPTER_FILES + ZUI_GENERATED_ENTRIES + [
20
+ TREE_SHAKE_REPORT,
21
+ QmlCompiler::COMPILED_ROOT,
22
+ QmlCompiler::REPORT,
23
+ QmlCompiler::CHECKSUM,
24
+ QmlCompiler::PROVENANCE,
25
+ "omarchy-ui-runtime"
26
+ ]).freeze
21
27
 
22
28
  def executable
23
29
  override = ENV["OMARCHY_UI_RUNTIME"]
@@ -39,7 +45,7 @@ module OmarchyUI
39
45
  FileUtils.rm_f(temporary) if temporary && File.exist?(temporary)
40
46
  end
41
47
 
42
- def install_package(path, framework_root: FRAMEWORK_ROOT, project: path)
48
+ def install_package(path, framework_root: FRAMEWORK_ROOT, project: path, compiled: false)
43
49
  FileUtils.mkdir_p(path)
44
50
  GENERATED_ENTRIES.each do |entry|
45
51
  generated = File.join(path, entry)
@@ -67,6 +73,7 @@ module OmarchyUI
67
73
  source = File.join(framework_root, "vendor", "runtime", "x86_64-linux", file)
68
74
  FileUtils.cp(source, File.join(path, file)) if File.file?(source)
69
75
  end
76
+ QmlCompiler.compile!(path) if compiled
70
77
  path
71
78
  end
72
79
 
data/lib/omarchy_ui.rb CHANGED
@@ -11,7 +11,7 @@ rescue LoadError
11
11
  end
12
12
 
13
13
  module OmarchyUI
14
- VERSION = "0.0.8"
14
+ VERSION = "0.0.9"
15
15
  ZUI_VERSION = Zui::VERSION
16
16
  FRAMEWORK_ROOT = File.expand_path("..", __dir__)
17
17
 
@@ -128,4 +128,5 @@ end
128
128
 
129
129
  require_relative "omarchy_ui/project_config"
130
130
  require_relative "omarchy_ui/generator"
131
+ require_relative "omarchy_ui/qml_compiler"
131
132
  require_relative "omarchy_ui/runtime"
@@ -17,4 +17,4 @@ The workflow checks out every external input at a full commit digest, performs t
17
17
  builds, requires byte-identical executables, and signs the resulting SHA through GitHub's
18
18
  artifact attestation service. Machine-readable provenance is included in
19
19
  `runtime-provenance.json`; independent verification instructions are in
20
- [`docs/runtime-build.md`](../../../docs/runtime-build.md).
20
+ the [runtime build documentation](https://github.com/AdamMusa/omarchy-ui/blob/main/docs/runtime-build.md).
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omarchy-ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.0.9
5
5
  platform: x86_64-linux
6
6
  authors:
7
7
  - Adam Moussa Ali
@@ -40,6 +40,7 @@ files:
40
40
  - lib/omarchy_ui/cli.rb
41
41
  - lib/omarchy_ui/generator.rb
42
42
  - lib/omarchy_ui/project_config.rb
43
+ - lib/omarchy_ui/qml_compiler.rb
43
44
  - lib/omarchy_ui/runtime.rb
44
45
  - vendor/runtime/x86_64-linux/RUNTIME_PROVENANCE.md
45
46
  - vendor/runtime/x86_64-linux/omarchy-ui-runtime