diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fd86348554..de8acf2b8a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -185,46 +185,36 @@ jobs:
- name: Environment
run: podman exec agama bash -c "env | sort"
- - name: Install Ruby gems
- run: podman exec agama bash -c "cd /checkout/service && bundle config set --local path 'vendor/bundle' && bundle install"
+ - name: Build the frontend
+ run: podman exec agama bash -c "cd /checkout/web && npm install && make"
- - name: Install the Agama D-Bus configuration
- run: podman exec agama bash -c "cp /checkout/service/share/dbus.conf /usr/share/dbus-1/system.d/org.opensuse.Agama.conf"
+ - name: Install the frontend
+ run: podman exec agama bash -c "ln -snfv /checkout/web/dist /usr/share/cockpit/agama"
- - name: Set a testing Agama configuration
- # copy a simplified ALP config file, it skips the product selection at the beginning
- run: podman exec agama bash -c "cp /checkout/playwright/config/agama.yaml /checkout/service/etc/agama.yaml"
+ # ./setup-service.sh will try setting up cockpit.socket
+ # which has a login page, so this local-session needs to be first
+ - name: Start Cockpit service
+ run: podman exec --detach agama /usr/libexec/cockpit-ws --local-session=/usr/bin/cockpit-bridge
- - name: Start NetworkManager
- # We need to run it manually as systemd dbus activation looks like failing
- run: podman exec agama /usr/sbin/NetworkManager
+ - name: Setup service
+ run: podman exec agama bash -c "cd /checkout; ./setup-service.sh"
- - name: Reload the D-Bus service
- run: podman exec agama systemctl reload dbus
+ - name: Rust unit tests
+ run: podman exec agama bash -c "cd /checkout/rust; cargo test --verbose"
- - name: Start the Agama D-Bus services
- # TODO: here is a potential race condition, but as building the frontend
- # takes quite long time it should never happen™
- run: podman exec agama bash -c "cd /checkout/service && (bundle exec bin/agamactl > service.log 2>&1 &)"
+ - name: Set a testing Agama configuration
+ # copy a simplified ALP config file, it skips the product selection at the beginning
+ run: podman exec agama bash -c "cp /checkout/playwright/config/agama.yaml /checkout/service/etc/agama.yaml"
- - name: Build the frontend
- run: podman exec agama bash -c "cd /checkout/web && npm install && make"
+ - name: Show NetworkManager log
+ run: podman exec agama journalctl -u NetworkManager
- name: Show the D-Bus services log
- run: podman exec agama cat /checkout/service/service.log
+ run: podman exec agama bash -c "journalctl | grep agama"
- name: Check DBus socket
run: podman exec agama ls -l /var/run/dbus/system_bus_socket
- - name: Show journal
- run: podman exec agama journalctl -b || echo "journal failed with $?"
-
- - name: Install the frontend
- run: podman exec agama bash -c "ln -snfv /checkout/web/dist /usr/share/cockpit/agama"
-
- - name: Start Cockpit service
- run: podman exec --detach agama /usr/libexec/cockpit-ws --local-session=/usr/bin/cockpit-bridge
-
- name: Run the Agama smoke test
run: podman exec agama curl http://localhost:9090/cockpit/@localhost/agama/index.html
@@ -236,6 +226,13 @@ jobs:
# run the tests in the Chromium browser
run: podman exec agama bash -c "cd /checkout/playwright && SKIP_LOGIN=true playwright test --trace on --project chromium"
+ - name: Again show the D-Bus services log
+ run: podman exec agama bash -c "journalctl | grep agama"
+
+ - name: Show the complete journal
+ # maybe not necessary if the above filtered journalctl calls are enough
+ run: podman exec agama journalctl -b || echo "journal failed with $?"
+
- name: Upload the test results
uses: actions/upload-artifact@v3
# run even when the previous step fails
@@ -247,24 +244,6 @@ jobs:
playwright/test-results/**/*
/tmp/log/YaST2/y2log
- cli_build:
- runs-on: ubuntu-latest
-
- env:
- CARGO_TERM_COLOR: always
-
- defaults:
- run:
- working-directory: ./rust
-
- steps:
- - uses: actions/checkout@v3
- - name: Build
- run: cargo build --verbose
- - name: Run tests
- run: cargo test --verbose
-
-
finish:
runs-on: ubuntu-latest
diff --git a/README.md b/README.md
index 61be537e8f..8d210fbb11 100644
--- a/README.md
+++ b/README.md
@@ -108,6 +108,10 @@ Alternatively you can run a development server which works as a proxy for
the cockpit server. See more details [in the documentation](
web/README.md#using-a-development-server).
+Another alternative is to run source checkout inside container so system is not
+affected by doing testing run beside real actions really done by installer.
+See more details [in the documentation][doc/testing_using_container.md].
+
* Start the services:
* beware that Agama must run as root (like YaST does) to do
hardware probing, partition the disks, install the software and so on.
diff --git a/doc/locale_api.md b/doc/locale_api.md
new file mode 100644
index 0000000000..ad8ceeaf4d
--- /dev/null
+++ b/doc/locale_api.md
@@ -0,0 +1,295 @@
+# Legacy-free Locale Service
+
+Problem statement: (2023-03)
+
+> Agama currently has a separate Language service, although it's rather
+> simplistic. It just allows to set the language of the installed system using
+> Yast::Language.Set. And it's quite memory demanding for such an unimpressive
+> task.
+
+> That service would be a nice candidate to be rewritten from scratch with no
+> dependencies on YaST or Ruby. It's small enough and could give us a good
+> overview on how much can we save.
+
+Original Plan:
+
+1. take the systemd APIs as a sensible starting point.
+2. deviate only where we add value
+
+The problem with the original plan is that
+the installer runs in one system (inst-sys, `/`)
+and operates on another (target, `/mnt`) and we cannot use the full systemd
+API. We may use `systemd-firstboot` instead but its API is much more limited.
+
+## Localization
+
+This design includes localized labels in the API. In other contexts that would
+be a responsibility of the frontend, but here the backend has the
+information, provided by _langtable_.
+
+(Languages, Territories and Timezones have localized names. Keyboards do not.)
+
+(Possible alternative: still include localized labels, but in a supplemental
+method while the main method only provides the IDs (and English labels))
+
+## Proposal
+
+A Proposal is what the installer proposes to the user
+as settings to be applied to the target system.
+
+For example, when selecting the "German (Germany)" locale,
+the timezone will be proposed to "Europe/Berlin".
+
+Design decision: put the proposal logic to the antecedent object, that is,
+the Locale object will know how to change the Timezone object,
+not the other way around (Timezone reacting to Locale value).
+
+### Overriding the User's Choice?
+
+
+
+If setting the locale proposes the keyboard, what do we do if the user first
+changes the keyboard and _then_ the locale?
+
+
+When Agama UI first shows up, it may show default choices like:
+
+> Locale: English (US), Keyboard: US
+
+Then we change the locale to Czech, and the keyboard is adjusted automatically:
+
+> Locale: Czech, Keyboard: Czech
+
+We tune the keyboard:
+
+> Locale: Czech, Keyboard: Czech (qwerty)
+
+When we then change the locale, the keyboard could stay the same, as we have
+already touched it:
+
+> Locale: German, Keyboard: Czech (qwerty)
+
+
+### Simple Design: Always Repropose
+
+We can easily afford throwing away the user's choice of keyboard layout and
+simply set what we consider a good default for a newly set locale, because:
+
+1. it is just one setting (as opposed to whole partitioning layout)
+2. the change will be visible in the UI, I assume
+
+### Detailed Design: Prioritize
+
+But other cases may not be as simple, so here's a generic design (NOT YET USED):
+
+All settings are wrapped in a `Priority` generic type (an Enum in Rust),
+meaning, what is the source and importance of the setting:
+- `Machine(data)` means the system has proposed it
+- `Human(data)` means the user has made the choice
+
+In D-Bus, it is represented by wrapping the data in a struct, with a leading
+byte* tagging the priority. For ease of recognition when watching bus traffic,
+special numbers are used:
+- `23` means Human, for the number of chromosome pairs
+- `42` means Machine, as the famous Answer was given by Deep Thought, a machine
+
+In the following dump, we see that the locale was set by the user and the
+system has adjusted the keyboard.
+
+```
+node ...Agama/Locale1 {
+ interface ...Agama.Locale1 {
+ properties:
+ readwrite (yas) Locale = (23, ['cs_CZ.UTF-8', 'de_DE.UTF-8']);
+ readwrite (y(ss)) X11Keyboard = (42, ('cz','qwerty));
+ };
+};
+```
+
+You may know a [similar settings in libzypp][resstatus] where it has 4 levels.
+
+*: maybe this is a crazy optimization? I am not too opposed to use strings for
+this on the bus.
+
+[resstatus]: https://github.com/openSUSE/libzypp/blob/d441746c59f063b5d54833bfdebc48829b07feb5/zypp/ResStatus.h#L106
+
+
+## Interfaces
+
+### Language and Keyboard
+
+- when setting the locale, adjust the proposed package selection and keyboard
+ accordingly. And timezone.
+
+The general design of the proposal layer is
+
+- declarative, using read-write properties
+- setting some properties will make changes in the proposal layer of other
+ properties of other objects
+
+I don't know: should the proposal be adjusted automatically as part of the property setter, or should it be explicit?
+
+So here, setting `Locale` below will set also `VConsoleKeyboard` here and
+ - Agama...Software...todo(...)
+ - Agama...Timezone...todo(...)
+
+For the first version of the API, let's keep things simple:
+
+**LocaleType** is just one string, the value for the `LANG` variable, like
+`"cs_CZ.UTF-8"`.
+
+**VConsoleKeyboardType** is a string, for example
+`"cz-qwerty"` or `"us"`.
+
+`systemd-firstboot` only has an option for the console keymap, but we have a
+way to propagate it to X11, see [bsc#1046436](https://bugzilla.suse.com/show_bug.cgi?id=1046436)
+
+We don't expose the X11 keyboard, instead letting systemd do it via the
+_convert_ parameter.
+
+(The other systemd keyboard settings are X11Model and X11Options, we don't
+have UI or data for that)
+
+NOTE: _langtable_ on the other hand only deals with the X11 keyboards,
+linking them to languages and territories.
+
+```
+# this is gdbus syntax BTW
+node /org/opensuse/Agama/Locale1 {
+ interface org.opensuse.Agama.Locale1 {
+ methods:
+ # In the same order as in SupportedLocales, pairs of
+ # (english_labels, native_labels), where foo_labels
+ # is a pair of (language, territory)
+ LabelsForLocales(
+ out a((ss)(ss)) id_english_native # [(("Spanish", "Spain"), ("Español", "España")), (('English', 'United States'), ('English', 'United States'))]
+ )
+ ListVConsoleKeyboards(
+ out as ids # like ["cz", "cz-qwerty", "gb-intl", "us", "us-dvorak",…]
+ )
+
+ # ProposeKeyboard(); # not needed? adjusted automatically, same object
+ # Sets Agama/TimeDate1's Timezone (but not LocalRTC, that's for Storage to say?)
+ ProposeTimeDate(); # different object but same service
+ ProposeSoftware(); # different service
+
+ Commit();
+ properties:
+
+ # The locale service DOES NOT KNOW which locales are
+ # available for the product currently selected for installation.
+ # When the user chooses a product, SupportedLocales should be set.
+ # It affects the output of LabelsForLocales
+ # and the valid inputs for Locales.
+ readwrite as SupportedLocales = ["es_ES.UTF-8", "en_US.UTF-8"];
+
+ # NOTE: "as" has different meaning to systemd,
+ # we have a list of LANG settings, 1st gets passed to systemd,
+ # others affect package selection
+ readwrite as Locales = ['cs_CZ.UTF-8', 'de_DE.UTF-8'];
+
+ readwrite s VConsoleKeyboard = 'cz-qwerty';
+ };
+};
+```
+
+#### Systemd
+
+
+
+For reference, the systemd API for Locale(Language) and Keyboard is this:
+
+
+```
+$ gdbus introspect -y -d org.freedesktop.locale1 -o /org/freedesktop/locale1
+node /org/freedesktop/locale1 {
+ interface org.freedesktop.locale1 {
+ methods:
+ SetLocale(in as locale,
+ in b interactive);
+ SetVConsoleKeyboard(in s keymap,
+ in s keymap_toggle,
+ in b convert,
+ in b interactive);
+ SetX11Keyboard(in s layout,
+ in s model,
+ in s variant,
+ in s options,
+ in b convert,
+ in b interactive);
+…
+$ busctl --system introspect org.freedesktop.locale1 /org/freedesktop/locale1
+(all properties are read-only and emit PropertiesChanged)
+.Locale property as 1 "LANG=en_US.UTF-8"
+.VConsoleKeymap property s "cz-lat2-us"
+.VConsoleKeymapToggle property s ""
+.X11Layout property s "cz,us"
+.X11Model property s "pc105"
+.X11Options property s "terminate:ctrl_alt_bksp,grp:shift_togg…
+.X11Variant property s "qwerty,basic"
+```
+
+
+
+### Timezone
+
+```
+node /org/opensuse/Agama/TimeDate1 {
+ interface org.opensuse.Agama.TimeDate1 {
+ methods:
+ ListTimezones(
+ in s display_locale # "de_DE.UTF-8"
+ out a(ss) id_label_pairs # [('Europe/Prague', 'Europa/Prag')]
+ )
+ # success? do we need a specific return value other than some Error?
+ Commit();
+ properties:
+ readwrite s Timezone = 'Europe/Prague';
+ readwrite b LocalRTC = false;
+ };
+};
+```
+
+#### Systemd
+
+
+
+For reference, the systemd API for Time and Timezone is this:
+
+
+(I find `gdbus` verbose output better for methods and `busctl` terse output
+better for properties)
+
+```
+$ gdbus introspect -y -d org.freedesktop.timedate1 -o /org/freedesktop/timedate1
+node /org/freedesktop/timedate1 { …
+ interface org.freedesktop.timedate1 { …
+ methods:
+ SetTime(in x usec_utc,
+ in b relative,
+ in b interactive);
+ SetTimezone(in s timezone,
+ in b interactive);
+ SetLocalRTC(in b local_rtc,
+ in b fix_system,
+ in b interactive);
+ SetNTP(in b use_ntp,
+ in b interactive);
+ ListTimezones(out as timezones);
+…
+$ busctl --system introspect org.freedesktop.timedate1 /org/freedesktop/timedate1
+NAME TYPE SIG RESULT/VALUE FLAGS
+(properties are read only)
+.CanNTP property b true -
+.LocalRTC property b false emits-change
+.NTP property b false emits-change
+.NTPSynchronized property b false -
+.RTCTimeUSec property t 1681214874000000 -
+.TimeUSec property t 1681214874046139 -
+.Timezone property s "Europe/Prague" emits-change
+```
+
+"LocalRTC" means "is the local time zone used for the real time clock",
+so it's !hwclock_in_UTC
+
+
diff --git a/doc/testing_using_container.md b/doc/testing_using_container.md
new file mode 100644
index 0000000000..e21d8ef68c
--- /dev/null
+++ b/doc/testing_using_container.md
@@ -0,0 +1,47 @@
+## Testing Using Container
+
+To test complex change that affects multiple parts of agama it is possible to
+run from sources using container that is used to run CI.
+
+Below is shell script that start container, provides web UI on port 9090 and
+also gives root access to container for more testing.
+
+```sh
+# https://build.opensuse.org/package/show/YaST:Head:Containers/agama-testing
+CIMAGE=registry.opensuse.org/yast/head/containers/containers_tumbleweed/opensuse/agama-testing:latest
+# rename this if you test multiple things
+CNAME=agama
+# the '?' here will report a shell error
+# if you accidentally paste a command without setting the variable first
+echo ${CNAME?}
+
+test -f service/agama.gemspec || echo "You should run this from a checkout of agama"
+
+# destroy the previous instance, can fail if there is no previous instance
+podman stop ${CNAME?}
+podman rm ${CNAME?}
+
+# Update our image
+podman pull ${CIMAGE?}
+
+podman run --name ${CNAME?} \
+ --privileged --detach --ipc=host \
+ -v .:/checkout \
+ -p 9090:9090 \
+ ${CIMAGE?}
+
+# shortcut for the following
+CEXEC="podman exec ${CNAME?} bash -c"
+
+${CEXEC?} "cd /checkout && ./setup.sh"
+
+# Now the CLI is in the same repo, just symlink it
+${CEXEC?} "ln -sfv /checkout/./rust/target/debug/agama /usr/bin/agama"
+
+# Manually start cockpit as socket activation does not work with port forwarding
+${CEXEC?} "systemctl start cockpit"
+
+# Optional: Interactive shell in the container
+podman exec --tty --interactive ${CNAME?} bash
+
+```
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index ad2bd8c210..eecc69875a 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 3
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
[[package]]
name = "agama-cli"
version = "1.0.0"
@@ -17,6 +23,17 @@ dependencies = [
"thiserror",
]
+[[package]]
+name = "agama-dbus-server"
+version = "0.1.0"
+dependencies = [
+ "agama-locale-data",
+ "anyhow",
+ "async-std",
+ "zbus",
+ "zbus_macros",
+]
+
[[package]]
name = "agama-derive"
version = "1.0.0"
@@ -44,6 +61,18 @@ dependencies = [
"zbus",
]
+[[package]]
+name = "agama-locale-data"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "chrono-tz",
+ "flate2",
+ "quick-xml",
+ "regex",
+ "serde",
+]
+
[[package]]
name = "ahash"
version = "0.8.3"
@@ -320,6 +349,38 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+[[package]]
+name = "chrono"
+version = "0.4.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "chrono-tz"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf9cc2b23599e6d7479755f3594285efb3f74a1bdca7a7374948bc831e23a552"
+dependencies = [
+ "chrono",
+ "chrono-tz-build",
+ "phf",
+]
+
+[[package]]
+name = "chrono-tz-build"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9998fb9f7e9b2111641485bf8beb32f92945f97f92a3d061f744cfef335f751"
+dependencies = [
+ "parse-zoneinfo",
+ "phf",
+ "phf_codegen",
+]
+
[[package]]
name = "clap"
version = "4.1.8"
@@ -397,6 +458,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "crc32fast"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+dependencies = [
+ "cfg-if",
+]
+
[[package]]
name = "crossbeam-utils"
version = "0.8.15"
@@ -570,6 +640,16 @@ dependencies = [
"instant",
]
+[[package]]
+name = "flate2"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
[[package]]
name = "form_urlencoded"
version = "1.1.0"
@@ -944,6 +1024,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+[[package]]
+name = "miniz_oxide"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
+dependencies = [
+ "adler",
+]
+
[[package]]
name = "nix"
version = "0.26.2"
@@ -1126,12 +1215,59 @@ dependencies = [
"windows-sys 0.45.0",
]
+[[package]]
+name = "parse-zoneinfo"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
+dependencies = [
+ "regex",
+]
+
[[package]]
name = "percent-encoding"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+[[package]]
+name = "phf"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c"
+dependencies = [
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf"
+dependencies = [
+ "phf_shared",
+ "rand",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676"
+dependencies = [
+ "siphasher",
+]
+
[[package]]
name = "pin-project-lite"
version = "0.2.9"
@@ -1221,6 +1357,16 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "quick-xml"
+version = "0.28.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
[[package]]
name = "quote"
version = "1.0.23"
@@ -1398,6 +1544,12 @@ dependencies = [
"digest",
]
+[[package]]
+name = "siphasher"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
+
[[package]]
name = "slab"
version = "0.4.8"
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index 8df317a4b6..4143977ed6 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -2,5 +2,7 @@
members = [
"agama-lib",
"agama-cli",
- "agama-derive"
+ "agama-derive",
+ "agama-locale-data",
+ "agama-dbus-server"
]
diff --git a/rust/README.md b/rust/README.md
index 565c496b25..df265fef77 100644
--- a/rust/README.md
+++ b/rust/README.md
@@ -1,8 +1,9 @@
-# Agama Command Line Interface
+# Agama Command Line and D-Bus Interface
This project aims to build a command-line interface for
[Agama](https://github.com/yast/agama), a service-based Linux installer featuring a nice
-web interface.
+web interface. The second aim is D-Bus service that does not depend heavily on YaST to
+reduce memory consumption and also provide better performance.
## Code organization
@@ -10,11 +11,14 @@ We have set up [Cargo workspace](https://doc.rust-lang.org/book/ch14-03-cargo-wo
three packages:
* [agama-lib](./agama-lib): code that can be reused to access the
- [Agama DBus API](https://github.com/yast/agama/blob/master/doc/dbus_api.md) and a
+ [Agama D-Bus API](https://github.com/yast/agama/blob/master/doc/dbus_api.md) and a
model for the configuration settings.
* [agama-cli](./agama-cli): code specific to the command line interface.
* [agama-derive](./agama-derive): includes a [procedural
macro](https://doc.rust-lang.org/reference/procedural-macros.html) to reduce the boilerplate code.
+* [agama-locale-data](./agama-locale-data): specific library to provide data for localization D-Bus
+ API
+* [agama-dbus-server](./agama-dbus-server): provides D-Bus API for services implemented in rust
## Status
@@ -24,6 +28,8 @@ Agama CLI is still a work in progress, although it is already capable of doing a
* Handling the auto-installation profiles.
* Triggering the *probing* and the *installation* processes.
+Agama D-Bus API is also a work in progress, but it is already used by Agama.
+
## Installation
You can grab the [RPM package](https://build.opensuse.org/package/show/YaST:Head:Agama/agama-cli) from
@@ -32,14 +38,17 @@ the [YaST:Head:Agama](https://build.opensuse.org/project/show/YaST:Head:Agama) p
If you prefer, you can install it from sources with [Cargo](https://doc.rust-lang.org/cargo/):
```
-git clone https://github.com/yast/agama-cli
+git clone https://github.com/openSUSE/agama
+cd rust
cargo install --path .
```
## Running
-Take into account that you need to run `agama-cli` as root when you want to query or change the
-Agama configuration. Assuming that the Agama D-Bus service is running, the next command
+For D-Bus API just run as root agama-dbus-server binary and it will properly attach to D-Bus.
+
+For CLI take into account that you need to run `agama-cli` as root when you want to query or change
+the Agama configuration. Assuming that the Agama D-Bus service is running, the next command
prints the current settings using JSON (hint: you can use `jq` to make result look better):
```
@@ -102,3 +111,18 @@ frontend](./agama-cli/doc/backend-for-testing.md)*.
## Caveats
* If no product is selected, the `probe` command fails.
+
+## Packaging
+
+Packaging files live in the `package' directory. Agama follows the
+[Rust packaging guidelines](https://en.opensuse.org/openSUSE:Packaging_Rust_Software).
+To test changes to the spec file, use a simple `osc branch YaST:Head:Agama agama-cli`.
+and copy the modified spec file to that branch.
+If it also needs specific code from a git branch, then modify `_service' file and
+put the git branch name in the `` tag. Then run `osc service runall`.
+
+Note: for openSUSE Leap, `cargo_audit` [does not work][c_a_bug] with older Python, so comment out that
+service section for the test build.
+For the test build, use the usual `osc build' on modified sources.
+
+[c_a_bug]: https://github.com/openSUSE/obs-service-cargo_audit/pull/6
diff --git a/rust/agama-dbus-server/Cargo.toml b/rust/agama-dbus-server/Cargo.toml
new file mode 100644
index 0000000000..6a38539f7c
--- /dev/null
+++ b/rust/agama-dbus-server/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "agama-dbus-server"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow = "1.0"
+agama-locale-data = { path="../agama-locale-data" }
+zbus = "3.7.0"
+zbus_macros = "3.7.0"
+async-std = { version = "1.12.0", features = ["attributes"]}
diff --git a/rust/agama-dbus-server/src/error.rs b/rust/agama-dbus-server/src/error.rs
new file mode 100644
index 0000000000..e2414d045d
--- /dev/null
+++ b/rust/agama-dbus-server/src/error.rs
@@ -0,0 +1,21 @@
+use zbus_macros::DBusError;
+
+#[derive(DBusError, Debug)]
+#[dbus_error(prefix = "org.opensuse.Agama.Locale1")]
+pub enum Error {
+ #[dbus_error(zbus_error)]
+ ZBus(zbus::Error),
+ Anyhow(String),
+}
+
+// This would be nice, but using it for a return type
+// results in a confusing error message about
+// error[E0277]: the trait bound `MyError: Serialize` is not satisfied
+//type MyResult = Result;
+
+impl From for Error {
+ fn from(e: anyhow::Error) -> Self {
+ // {:#} includes causes
+ Self::Anyhow(format!("{:#}", e))
+ }
+}
diff --git a/rust/agama-dbus-server/src/locale.rs b/rust/agama-dbus-server/src/locale.rs
new file mode 100644
index 0000000000..06ecfa85b7
--- /dev/null
+++ b/rust/agama-dbus-server/src/locale.rs
@@ -0,0 +1,199 @@
+use crate::error::Error;
+use anyhow::Context;
+use std::process::Command;
+use zbus::{dbus_interface, Connection, ConnectionBuilder};
+
+pub struct Locale {
+ locales: Vec,
+ keymap: String,
+ timezone_id: String,
+ supported_locales: Vec,
+}
+
+#[dbus_interface(name = "org.opensuse.Agama.Locale1")]
+impl Locale {
+ // Can be `async` as well.
+ /// get labels for given locale. The first pair is english language and territory
+ /// and second one is localized one to target language from locale.
+ ///
+ /// Note: check how often it is used and if often, it can be easily cached
+ fn labels_for_locales(&self) -> Result, Error> {
+ const DEFAULT_LANG: &str = "en";
+ let mut res = Vec::with_capacity(self.supported_locales.len());
+ let languages = agama_locale_data::get_languages()?;
+ let territories = agama_locale_data::get_territories()?;
+ for locale in self.supported_locales.as_slice() {
+ let (loc_language, loc_territory) = agama_locale_data::parse_locale(locale.as_str())?;
+
+ let language = languages.find_by_id(loc_language)
+ .context("language for passed locale not found")?;
+ let territory = territories.find_by_id(loc_territory)
+ .context("territory for passed locale not found")?;
+
+ let default_ret = (
+ language
+ .names
+ .name_for(DEFAULT_LANG)
+ .context("missing default translation for language")?,
+ territory
+ .names
+ .name_for(DEFAULT_LANG)
+ .context("missing default translation for territory")?,
+ );
+ let localized_ret = (
+ language
+ .names
+ .name_for(language.id.as_str())
+ .context("missing native label for language")?,
+ territory
+ .names
+ .name_for(language.id.as_str())
+ .context("missing native label for territory")?,
+ );
+ res.push((default_ret, localized_ret));
+ }
+
+ Ok(res)
+ }
+
+ #[dbus_interface(property)]
+ fn locales(&self) -> Vec {
+ return self.locales.to_owned();
+ }
+
+ #[dbus_interface(property)]
+ fn set_locales(&mut self, locales: Vec) -> zbus::fdo::Result<()> {
+ for loc in &locales {
+ if !self.supported_locales.contains(loc) {
+ return Err(zbus::fdo::Error::Failed(format!("Unsupported locale value '{loc}'")))
+ }
+ }
+ self.locales = locales;
+ Ok(())
+ }
+
+ #[dbus_interface(property)]
+ fn supported_locales(&self) -> Vec {
+ self.supported_locales.to_owned()
+ }
+
+ #[dbus_interface(property)]
+ fn set_supported_locales(&mut self, locales: Vec) -> Result<(), zbus::fdo::Error> {
+ self.supported_locales = locales;
+ // TODO: handle if current selected locale contain something that is no longer supported
+ Ok(())
+ }
+
+ /* support only keymaps for console for now
+ fn list_x11_keyboards(&self) -> Result, Error> {
+ let keyboards = agama_locale_data::get_xkeyboards()?;
+ let ret = keyboards
+ .keyboard.iter()
+ .map(|k| (k.id.clone(), k.description.clone()))
+ .collect();
+ Ok(ret)
+ }
+
+ fn set_x11_keyboard(&mut self, keyboard: &str) {
+ self.keyboard_id = keyboard.to_string();
+ }
+ */
+
+ #[dbus_interface(name="ListVConsoleKeyboards")]
+ fn list_keyboards(&self) -> Result, Error> {
+ let res = agama_locale_data::get_key_maps()?;
+ Ok(res)
+ }
+
+ #[dbus_interface(property, name="VConsoleKeyboard")]
+ fn keymap(&self) -> &str {
+ return &self.keymap.as_str();
+ }
+
+ #[dbus_interface(property, name="VConsoleKeyboard")]
+ fn set_keymap(&mut self, keyboard: &str) -> Result<(), zbus::fdo::Error> {
+ let exist = agama_locale_data::get_key_maps().unwrap().iter().find(|&k| k == keyboard).is_some();
+ if !exist {
+ return Err(zbus::fdo::Error::Failed("Invalid keyboard value".to_string()))
+ }
+ self.keymap = keyboard.to_string();
+ Ok(())
+ }
+
+ fn list_timezones(&self, locale: &str) -> Result, Error> {
+ let timezones = agama_locale_data::get_timezones();
+ let localized =
+ agama_locale_data::get_timezone_parts()?.localize_timezones(locale, &timezones);
+ let ret = timezones.into_iter().zip(localized.into_iter()).collect();
+ Ok(ret)
+ }
+
+ #[dbus_interface(property)]
+ fn timezone(&self) -> &str {
+ return &self.timezone_id.as_str();
+ }
+
+ #[dbus_interface(property)]
+ fn set_timezone(&mut self, timezone: &str) -> Result<(), zbus::fdo::Error> { // NOTE: cannot use crate::Error as property expect this one
+ self.timezone_id = timezone.to_string();
+ Ok(())
+ }
+
+ // TODO: what should be returned value for commit?
+ fn commit(&mut self) -> Result<(), Error> {
+ const ROOT: &str = "/mnt";
+ Command::new("/usr/bin/systemd-firstboot")
+ .args(["root", ROOT, "--locale", self.locales.first().context("missing locale")?.as_str()])
+ .status()
+ .context("Failed to execute systemd-firstboot")?;
+ Command::new("/usr/bin/systemd-firstboot")
+ .args(["root", ROOT, "--keymap", self.keymap.as_str()])
+ .status()
+ .context("Failed to execute systemd-firstboot")?;
+ Command::new("/usr/bin/systemd-firstboot")
+ .args(["root", ROOT, "--timezone", self.timezone_id.as_str()])
+ .status()
+ .context("Failed to execute systemd-firstboot")?;
+
+ Ok(())
+ }
+}
+
+
+impl Locale {
+ fn new() -> Locale {
+ Locale {
+ locales: vec!["en_US.UTF-8".to_string()],
+ keymap: "us".to_string(),
+ timezone_id: "America/Los_Angeles".to_string(),
+ supported_locales: vec!["en_US.UTF-8".to_string()],
+ }
+ }
+
+ pub async fn start_service() -> Result> {
+ const ADDRESS : &str = "unix:path=/run/agama/bus";
+ const SERVICE_NAME: &str = "org.opensuse.Agama.Locale1";
+ const SERVICE_PATH: &str = "/org/opensuse/Agama/Locale1";
+
+ // First connect to the Agama bus, then serve our API,
+ // for better error reporting.
+ let conn = ConnectionBuilder::address(ADDRESS)?
+ .build()
+ .await
+ .context(format!("Connecting to the Agama bus at {ADDRESS}"))?;
+
+ // When serving, request the service name _after_ exposing the main object
+ let locale = Locale::new();
+ conn
+ .object_server()
+ .at(SERVICE_PATH, locale)
+ .await?;
+ conn
+ .request_name(SERVICE_NAME)
+ .await
+ .context(format!("Requesting name {SERVICE_NAME}"))?;
+
+ Ok(conn)
+ }
+}
+
diff --git a/rust/agama-dbus-server/src/main.rs b/rust/agama-dbus-server/src/main.rs
new file mode 100644
index 0000000000..39aa557235
--- /dev/null
+++ b/rust/agama-dbus-server/src/main.rs
@@ -0,0 +1,14 @@
+pub mod error;
+pub mod locale;
+
+use std::future::pending;
+
+#[async_std::main]
+async fn main() -> Result<(), Box> {
+ let _con = crate::locale::Locale::start_service().await?;
+
+ // Do other things or go to wait forever
+ pending::<()>().await;
+
+ Ok(())
+}
diff --git a/rust/agama-lib/src/lib.rs b/rust/agama-lib/src/lib.rs
index c4803e1923..ab085a5f7b 100644
--- a/rust/agama-lib/src/lib.rs
+++ b/rust/agama-lib/src/lib.rs
@@ -16,8 +16,8 @@ use crate::error::ServiceError;
use anyhow::Context;
pub async fn connection() -> Result {
- let path = "/run/agama/bus";
- let address = format!("unix:path={path}");
+ const PATH : &str = "/run/agama/bus";
+ let address : String = format!("unix:path={PATH}");
let conn = zbus::ConnectionBuilder::address(address.as_str())?
.build()
.await
diff --git a/rust/agama-locale-data/Cargo.toml b/rust/agama-locale-data/Cargo.toml
new file mode 100644
index 0000000000..46380041c5
--- /dev/null
+++ b/rust/agama-locale-data/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "agama-locale-data"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow = "1.0"
+serde = { version = "1.0.152", features = ["derive"] }
+quick-xml = { version = "0.28.2", features = ["serialize"] }
+flate2 = "1.0.25"
+chrono-tz = "0.8.2"
+regex = "1"
diff --git a/rust/agama-locale-data/src/deprecated_timezones.rs b/rust/agama-locale-data/src/deprecated_timezones.rs
new file mode 100644
index 0000000000..403f210859
--- /dev/null
+++ b/rust/agama-locale-data/src/deprecated_timezones.rs
@@ -0,0 +1,173 @@
+/// List of timezones which are deprecated and langtables missing translations for it
+///
+/// Filtering it out also helps with returning smaller list of real timezones.
+/// Sadly many libraries facing issues with deprecated timezones, see e.g.
+///
+pub(crate) const DEPRECATED_TIMEZONES : &[&str]= &[
+ "Africa/Asmera", // replaced by Africa/Asmara
+ "Africa/Timbuktu", // replaced by Africa/Bamako
+ "America/Argentina/ComodRivadavia", // replaced by America/Argentina/Catamarca
+ "America/Atka", // replaced by America/Adak
+ "America/Ciudad_Juarez", // failed to find replacement
+ "America/Coral_Harbour", // replaced by America/Atikokan
+ "America/Ensenada", // replaced by America/Tijuana
+ "America/Fort_Nelson",
+ "America/Fort_Wayne", // replaced by America/Indiana/Indianapolis
+ "America/Knox_IN", // replaced by America/Indiana/Knox
+ "America/Nuuk",
+ "America/Porto_Acre", // replaced by America/Rio_Branco
+ "America/Punta_Arenas",
+ "America/Rosario",
+ "America/Virgin",
+ "Antarctica/Troll",
+ "Asia/Ashkhabad", // looks like typo/wrong transcript, it should be Asia/Ashgabat
+ "Asia/Atyrau",
+ "Asia/Barnaul",
+ "Asia/Calcutta", // renamed to Asia/Kolkata
+ "Asia/Chita",
+ "Asia/Chungking",
+ "Asia/Dacca",
+ "Asia/Famagusta",
+ "Asia/Katmandu",
+ "Asia/Macao",
+ "Asia/Qostanay",
+ "Asia/Saigon",
+ "Asia/Srednekolymsk",
+ "Asia/Tel_Aviv",
+ "Asia/Thimbu",
+ "Asia/Tomsk",
+ "Asia/Ujung_Pandang",
+ "Asia/Ulan_Bator",
+ "Asia/Yangon",
+ "Atlantic/Faeroe",
+ "Atlantic/Jan_Mayen",
+ "Australia/ACT",
+ "Australia/Canberra",
+ "Australia/LHI",
+ "Australia/NSW",
+ "Australia/North",
+ "Australia/Queensland",
+ "Australia/South",
+ "Australia/Tasmania",
+ "Australia/Victoria",
+ "Australia/West",
+ "Australia/Yancowinna",
+ "Brazil/Acre",
+ "Brazil/DeNoronha",
+ "Brazil/East",
+ "Brazil/West",
+ "CET",
+ "CST6CDT",
+ "Canada/Atlantic", // all canada TZ was replaced by America ones
+ "Canada/Central",
+ "Canada/Eastern",
+ "Canada/Mountain",
+ "Canada/Newfoundland",
+ "Canada/Pacific",
+ "Canada/Saskatchewan",
+ "Canada/Yukon",
+ "Chile/Continental", // all Chile was replaced by continental America tz
+ "Chile/EasterIsland",
+ "Cuba",
+ "EET",
+ "EST", // not sure why it is not in langtable
+ "EST5EDT",
+ "Egypt",
+ "Eire",
+ "Etc/GMT",
+ "Etc/GMT+0",
+ "Etc/GMT+1",
+ "Etc/GMT+2",
+ "Etc/GMT+3",
+ "Etc/GMT+4",
+ "Etc/GMT+5",
+ "Etc/GMT+6",
+ "Etc/GMT+7",
+ "Etc/GMT+8",
+ "Etc/GMT+9",
+ "Etc/GMT+10",
+ "Etc/GMT+11",
+ "Etc/GMT+12",
+ "Etc/GMT-0",
+ "Etc/GMT-1",
+ "Etc/GMT-2",
+ "Etc/GMT-3",
+ "Etc/GMT-4",
+ "Etc/GMT-5",
+ "Etc/GMT-6",
+ "Etc/GMT-7",
+ "Etc/GMT-8",
+ "Etc/GMT-9",
+ "Etc/GMT-10",
+ "Etc/GMT-11",
+ "Etc/GMT-12",
+ "Etc/GMT-13",
+ "Etc/GMT-14",
+ "Etc/GMT0",
+ "Etc/Greenwich",
+ "Etc/UCT",
+ "Etc/UTC",
+ "Etc/Universal",
+ "Etc/Zulu",
+ "Europe/Astrakhan",
+ "Europe/Belfast",
+ "Europe/Kirov",
+ "Europe/Kyiv",
+ "Europe/Saratov",
+ "Europe/Tiraspol",
+ "Europe/Ulyanovsk",
+ "GB",
+ "GB-Eire",
+ "GMT",
+ "GMT+0",
+ "GMT-0",
+ "GMT0",
+ "Greenwich",
+ "HST",
+ "Hongkong",
+ "Iceland",
+ "Iran",
+ "Israel",
+ "Jamaica",
+ "Japan",
+ "Kwajalein",
+ "Libya",
+ "MET",
+ "Mexico/BajaNorte",
+ "Mexico/BajaSur",
+ "Mexico/General",
+ "MST",
+ "MST7MDT",
+ "NZ",
+ "NZ-CHAT",
+ "Navajo",
+ "Pacific/Bougainville",
+ "Pacific/Kanton",
+ "Pacific/Ponape",
+ "Pacific/Samoa",
+ "Pacific/Truk",
+ "Pacific/Yap",
+ "PRC",
+ "PST8PDT",
+ "Poland",
+ "Portugal",
+ "ROC",
+ "ROK",
+ "Singapore",
+ "Turkey",
+ "UCT",
+ "Universal",
+ "US/Aleutian", // all US/ replaced by America
+ "US/Central",
+ "US/East-Indiana",
+ "US/Eastern",
+ "US/Hawaii",
+ "US/Indiana-Starke",
+ "US/Michigan",
+ "US/Mountain",
+ "US/Pacific",
+ "US/Samoa",
+ "W-SU",
+ "WET",
+ "Zulu",
+];
\ No newline at end of file
diff --git a/rust/agama-locale-data/src/language.rs b/rust/agama-locale-data/src/language.rs
new file mode 100644
index 0000000000..8502459307
--- /dev/null
+++ b/rust/agama-locale-data/src/language.rs
@@ -0,0 +1,23 @@
+use serde::Deserialize;
+
+use crate::ranked::{RankedTerritories, RankedLocales};
+
+#[derive(Debug, Deserialize)]
+pub struct Language {
+ #[serde(rename(deserialize = "languageId"))]
+ pub id: String,
+ pub territories: RankedTerritories,
+ pub locales: RankedLocales,
+ pub names: crate::localization::Localization
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Languages {
+ pub language: Vec
+}
+
+impl Languages {
+ pub fn find_by_id(&self, id: &str) -> Option<&Language> {
+ self.language.iter().find(|t| t.id == id)
+ }
+}
\ No newline at end of file
diff --git a/rust/agama-locale-data/src/lib.rs b/rust/agama-locale-data/src/lib.rs
new file mode 100644
index 0000000000..4a10c2489d
--- /dev/null
+++ b/rust/agama-locale-data/src/lib.rs
@@ -0,0 +1,158 @@
+use anyhow::Context;
+use serde::Deserialize;
+use std::fs::File;
+use std::io::BufRead;
+use std::io::BufReader;
+use std::process::Command;
+use quick_xml::de::Deserializer;
+use flate2::bufread::GzDecoder;
+use regex::Regex;
+
+pub mod xkeyboard;
+pub mod language;
+pub mod localization;
+pub mod territory;
+pub mod timezone_part;
+pub mod ranked;
+pub mod deprecated_timezones;
+
+fn file_reader(file_path: &str) -> anyhow::Result {
+ let file = File::open(file_path)
+ .with_context(|| format!("Failed to read langtable-data ({})", file_path))?;
+ let reader = BufReader::new(GzDecoder::new(BufReader::new(file)));
+ Ok(reader)
+}
+
+/// Gets list of X11 keyboards structs
+pub fn get_xkeyboards() -> anyhow::Result {
+ const FILE_PATH: &str = "/usr/share/langtable/data/keyboards.xml.gz";
+ let reader = file_reader(FILE_PATH)?;
+ let mut deserializer = Deserializer::from_reader(reader);
+ let ret = xkeyboard::XKeyboards::deserialize(&mut deserializer)
+ .context("Failed to deserialize keyboard entry")?;
+ Ok(ret)
+}
+
+/// Gets list of available keymaps
+///
+/// ## Examples
+/// Requires working localectl.
+///
+/// ```no_run
+/// let key_maps = agama_locale_data::get_key_maps().unwrap();
+/// assert!(key_maps.contains(&"us".to_string()))
+/// ```
+pub fn get_key_maps() -> anyhow::Result> {
+ const BINARY: &str = "/usr/bin/localectl";
+ let output = Command::new(BINARY).arg("list-keymaps")
+ .output().context("failed to execute localectl list-maps")?.stdout;
+ let output = String::from_utf8(output).context("Strange localectl output formatting")?;
+ let ret = output.split('\n').map(|l| l.trim().to_string()).collect();
+
+ Ok(ret)
+}
+
+/// Parses given locale to language and territory part
+///
+/// /// ## Examples
+///
+/// ```
+/// let result = agama_locale_data::parse_locale("en_US.UTF-8").unwrap();
+/// assert_eq!(result.0, "en");
+/// assert_eq!(result.1, "US")
+/// ```
+pub fn parse_locale(locale: &str) -> anyhow::Result<(&str, &str)> {
+ let locale_regexp : Regex = Regex::new(r"^([[:alpha:]]+)_([[:alpha:]]+)").unwrap();
+ let captures = locale_regexp.captures(locale).context("Failed to parse locale")?;
+ Ok((captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str()))
+}
+
+/// Returns struct which contain list of known languages
+pub fn get_languages() -> anyhow::Result {
+ const FILE_PATH: &str = "/usr/share/langtable/data/languages.xml.gz";
+ let reader = file_reader(FILE_PATH)?;
+ let mut deserializer = Deserializer::from_reader(reader);
+ let ret = language::Languages::deserialize(&mut deserializer)
+ .context("Failed to deserialize language entry")?;
+ Ok(ret)
+}
+
+/// Returns struct which contain list of known territories
+pub fn get_territories() -> anyhow::Result {
+ const FILE_PATH: &str = "/usr/share/langtable/data/territories.xml.gz";
+ let reader = file_reader(FILE_PATH)?;
+ let mut deserializer = Deserializer::from_reader(reader);
+ let ret = territory::Territories::deserialize(&mut deserializer)
+ .context("Failed to deserialize territory entry")?;
+ Ok(ret)
+}
+
+/// Returns struct which contain list of known parts of timezones. Useful for translation
+pub fn get_timezone_parts() -> anyhow::Result {
+ const FILE_PATH: &str = "/usr/share/langtable/data/timezoneidparts.xml.gz";
+ let reader = file_reader(FILE_PATH)?;
+ let mut deserializer = Deserializer::from_reader(reader);
+ let ret = timezone_part::TimezoneIdParts::deserialize(&mut deserializer)
+ .context("Failed to deserialize timezone part entry")?;
+ Ok(ret)
+}
+
+
+/// Gets list of non-deprecated timezones
+pub fn get_timezones() -> Vec {
+ chrono_tz::TZ_VARIANTS.iter()
+ .filter(|&tz| !crate::deprecated_timezones::DEPRECATED_TIMEZONES.contains(&tz.name())) // Filter out deprecated asmera
+ .map(|e| e.name().to_string())
+ .collect()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_get_keyboards() {
+ let result = get_xkeyboards().unwrap();
+ let first = result.keyboard.first().expect("no keyboards");
+ assert_eq!(first.id, "ad")
+ }
+
+ #[test]
+ fn test_get_languages() {
+ let result = get_languages().unwrap();
+ let first = result.language.first().expect("no keyboards");
+ assert_eq!(first.id, "aa")
+ }
+
+ #[test]
+ fn test_get_territories() {
+ let result = get_territories().unwrap();
+ let first = result.territory.first().expect("no keyboards");
+ assert_eq!(first.id, "001") // looks strange, but it is meta id for whole world
+ }
+
+ #[test]
+ fn test_get_timezone_parts() {
+ let result = get_timezone_parts().unwrap();
+ let first = result.timezone_part.first().expect("no keyboards");
+ assert_eq!(first.id, "Abidjan")
+ }
+
+ #[test]
+ fn test_get_timezones() {
+ let result = get_timezones();
+ assert_eq!(result.len(), 430);
+ let first = result.first().expect("no keyboards");
+ assert_eq!(first, "Africa/Abidjan");
+ // test that we filter out deprecates Asmera ( there is already recent Asmara)
+ let asmera = result.iter().find(|&t| *t == "Africa/Asmera".to_string());
+ assert_eq!(asmera, None);
+ let asmara = result.iter().find(|&t| *t == "Africa/Asmara".to_string());
+ assert_eq!(asmara, Some(&"Africa/Asmara".to_string()));
+ // here test that timezones from timezones matches ones in langtable ( as timezones can contain deprecated ones)
+ // so this test catch if there is new zone that is not translated or if a zone is become deprecated
+ let timezones = get_timezones();
+ let localized = get_timezone_parts().unwrap().localize_timezones("de", &timezones);
+ let _res : Vec<(String, String)> = timezones.into_iter().zip(localized.into_iter()).collect();
+ }
+}
diff --git a/rust/agama-locale-data/src/localization.rs b/rust/agama-locale-data/src/localization.rs
new file mode 100644
index 0000000000..a3622589a9
--- /dev/null
+++ b/rust/agama-locale-data/src/localization.rs
@@ -0,0 +1,22 @@
+use serde::Deserialize;
+
+#[derive(Debug, Deserialize)]
+pub struct Localization {
+ pub name: Vec
+}
+
+impl Localization {
+ pub fn name_for(&self, language: &str) -> Option {
+ let entry = self.name.iter()
+ .find(|n| n.language == language)?;
+ Some(entry.value.clone())
+ }
+}
+
+#[derive(Debug, Deserialize)]
+pub struct LocalizationEntry {
+ #[serde(rename(deserialize = "languageId"))]
+ pub language: String,
+ #[serde(rename(deserialize = "trName"))]
+ pub value: String
+}
\ No newline at end of file
diff --git a/rust/agama-locale-data/src/ranked.rs b/rust/agama-locale-data/src/ranked.rs
new file mode 100644
index 0000000000..f5d26a2c60
--- /dev/null
+++ b/rust/agama-locale-data/src/ranked.rs
@@ -0,0 +1,43 @@
+//! Bigger rank means it is more important
+use serde::Deserialize;
+
+#[derive(Debug, Deserialize)]
+pub struct RankedLanguage {
+ #[serde(rename(deserialize = "languageId"))]
+ pub id: String,
+ /// Bigger rank means it is more important
+ pub rank: u16
+}
+
+#[derive(Debug, Deserialize)]
+pub struct RankedLanguages {
+ #[serde(default)]
+ pub language: Vec
+}
+
+#[derive(Debug, Deserialize)]
+pub struct RankedTerritory {
+ #[serde(rename(deserialize = "territoryId"))]
+ pub id: String,
+ /// Bigger rank means it is more important
+ pub rank: u16
+}
+
+#[derive(Debug, Deserialize)]
+pub struct RankedTerritories {
+ #[serde(default)]
+ pub territory: Vec
+}
+
+#[derive(Debug, Deserialize)]
+pub struct RankedLocale {
+ #[serde(rename(deserialize = "localeId"))]
+ pub id: String,
+ pub rank: u16
+}
+
+#[derive(Debug, Deserialize)]
+pub struct RankedLocales {
+ #[serde(default)]
+ pub locale: Vec
+}
diff --git a/rust/agama-locale-data/src/territory.rs b/rust/agama-locale-data/src/territory.rs
new file mode 100644
index 0000000000..209afbe2b5
--- /dev/null
+++ b/rust/agama-locale-data/src/territory.rs
@@ -0,0 +1,20 @@
+use serde::Deserialize;
+
+#[derive(Debug, Deserialize)]
+pub struct Territory {
+ #[serde(rename(deserialize = "territoryId"))]
+ pub id: String,
+ pub languages: crate::ranked::RankedLanguages,
+ pub names: crate::localization::Localization
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Territories {
+ pub territory: Vec
+}
+
+impl Territories {
+ pub fn find_by_id(&self, id: &str) -> Option<&Territory> {
+ self.territory.iter().find(|t| t.id == id)
+ }
+}
\ No newline at end of file
diff --git a/rust/agama-locale-data/src/timezone_part.rs b/rust/agama-locale-data/src/timezone_part.rs
new file mode 100644
index 0000000000..256fd3000c
--- /dev/null
+++ b/rust/agama-locale-data/src/timezone_part.rs
@@ -0,0 +1,66 @@
+use std::collections::HashMap;
+
+use serde::Deserialize;
+
+#[derive(Debug, Deserialize)]
+pub struct TimezoneIdPart {
+ #[serde(rename(deserialize = "timezoneIdPartId"))]
+ /// "Prague"
+ pub id: String,
+ /// [{language: "cs", value: "Praha"}, {"language": "de", value: "Prag"} ...]
+ pub names: crate::localization::Localization,
+}
+
+// Timezone id parts are useful mainly for localization of timezones
+// Just search each part of timezone for translation
+#[derive(Debug, Deserialize)]
+pub struct TimezoneIdParts {
+ #[serde(rename(deserialize = "timezoneIdPart"))]
+ pub timezone_part: Vec,
+}
+
+impl TimezoneIdParts {
+ /// Localized given list of timezones to given language
+ /// # Examples
+ ///
+ /// ```
+ /// let parts = agama_locale_data::get_timezone_parts().expect("missing timezone parts");
+ /// let timezones = vec!["Europe/Prague".to_string(), "Europe/Berlin".to_string()];
+ /// let result = vec!["Evropa/Praha".to_string(), "Evropa/Berlín".to_string()];
+ /// assert_eq!(parts.localize_timezones("cs", &timezones), result);
+ /// ```
+ pub fn localize_timezones(&self, language: &str, timezones: &Vec) -> Vec {
+ let mapping = self.construct_mapping(language);
+ timezones
+ .iter()
+ .map(|tz| self.translate_timezone(&mapping, tz))
+ .collect()
+ }
+
+ fn construct_mapping(&self, language: &str) -> HashMap {
+ let mut res: HashMap = HashMap::with_capacity(self.timezone_part.len());
+ self.timezone_part
+ .iter()
+ .map(|part| (part.id.clone(), part.names.name_for(language)))
+ .for_each(|(time_id, names)| -> () {
+ // skip missing translations
+ if let Some(trans) = names {
+ res.insert(time_id, trans);
+ }
+ });
+ return res;
+ }
+
+ fn translate_timezone(&self, mapping: &HashMap, timezone: &str) -> String {
+ timezone
+ .split("/")
+ .map(|tzp| {
+ mapping
+ .get(&tzp.to_string())
+ .expect(format!("Unknown timezone part {tzp}").as_str())
+ .to_owned()
+ })
+ .collect::>()
+ .join("/")
+ }
+}
diff --git a/rust/agama-locale-data/src/xkeyboard.rs b/rust/agama-locale-data/src/xkeyboard.rs
new file mode 100644
index 0000000000..b762aead83
--- /dev/null
+++ b/rust/agama-locale-data/src/xkeyboard.rs
@@ -0,0 +1,21 @@
+use serde::Deserialize;
+
+use crate::ranked::{RankedTerritories, RankedLanguages};
+
+#[derive(Debug, Deserialize)]
+pub struct XKeyboard {
+ #[serde(rename(deserialize = "keyboardId"))]
+ /// like "layout(variant)", for example "us" or "ua(phonetic)"
+ pub id: String,
+ /// like "Ukrainian (phonetic)"
+ pub description: String,
+ pub ascii: bool,
+ pub comment: Option,
+ pub languages: RankedLanguages,
+ pub territories: RankedTerritories
+}
+
+#[derive(Debug, Deserialize)]
+pub struct XKeyboards {
+ pub keyboard: Vec
+}
diff --git a/rust/package/agama-cli.spec b/rust/package/agama-cli.spec
index baf616ffa7..b57bca80c1 100644
--- a/rust/package/agama-cli.spec
+++ b/rust/package/agama-cli.spec
@@ -23,19 +23,35 @@ Summary: Agama command line interface
# If you know the license, put it's SPDX string here.
# Alternately, you can use cargo lock2rpmprovides to help generate this.
License: GPL-2.0-only
-Url: https://github.com/yast/agama-cli
+Url: https://github.com/opensuse/agama
Source0: agama.tar
Source1: vendor.tar.zst
# Generated by the cargo_vendor OBS service
Source2: cargo_config
BuildRequires: cargo-packaging
BuildRequires: pkgconfig(openssl)
+# used in tests for dbus service
+BuildRequires: python-langtable-data
+BuildRequires: dbus-1-common
Requires: jsonnet
Requires: lshw
%description
Command line program to interact with the agama service.
+%package -n agama-dbus-server
+# This will be set by osc services, that will run after this.
+Version: 0
+Release: 0
+Summary: Agama Rust D-Bus service
+License: GPL-2.0-only
+Url: https://github.com/opensuse/agama
+Requires: python-langtable-data
+Requires: dbus-1-common
+
+%description -n agama-dbus-server
+DBus service for agama project. It provides so far localization service.
+
%prep
%autosetup -a1 -n agama
mkdir .cargo
@@ -49,8 +65,12 @@ cp %{SOURCE2} .cargo/config
%install
install -D -d -m 0755 %{buildroot}%{_bindir}
install -m 0755 %{_builddir}/agama/target/release/agama %{buildroot}%{_bindir}/agama
+install -m 0755 %{_builddir}/agama/target/release/agama %{buildroot}%{_bindir}/agama-dbus-server
install -D -d -m 0755 %{buildroot}%{_datadir}/agama-cli
install -m 0644 %{_builddir}/agama/agama-lib/share/profile.schema.json %{buildroot}%{_datadir}/agama-cli
+install --directory %{buildroot}%{_datadir}/dbus-1/agama-services
+install -m 0644 --target-directory=%{buildroot}%{_datadir}/dbus-1/agama-services %{_builddir}/agama/share/*.service
+
%check
%{cargo_test}
@@ -60,4 +80,8 @@ install -m 0644 %{_builddir}/agama/agama-lib/share/profile.schema.json %{buildro
%dir %{_datadir}/agama-cli
%{_datadir}/agama-cli/profile.schema.json
+%files -n agama-dbus-server
+%{_bindir}/agama-dbus-server
+%{_datadir}/dbus-1/agama-services
+
%changelog
diff --git a/rust/share/org.opensuse.Agama.Locale1.service b/rust/share/org.opensuse.Agama.Locale1.service
new file mode 100644
index 0000000000..35ed50eef0
--- /dev/null
+++ b/rust/share/org.opensuse.Agama.Locale1.service
@@ -0,0 +1,4 @@
+[D-BUS Service]
+Name=org.opensuse.Agama.Locale1
+Exec=/usr/bin/agama-dbus-server
+User=root
diff --git a/service/bin/agamactl b/service/bin/agamactl
index 930a02e4c0..5caee1a663 100755
--- a/service/bin/agamactl
+++ b/service/bin/agamactl
@@ -63,33 +63,16 @@ def start_service(name)
service_runner.run
end
-ORDERED_SERVICES = [:questions, :language, :software, :storage, :users, :manager].freeze
-
-# Normally the services are started by D-Bus activation.
-# This starts all the services sequentially without relying on that.
-# This is useful during development to have their log output on the terminal,
-# not mixed with other syslog/journal messages.
-# The downside is relying on an arbitrary delay, and a manually maintained ordering.
-#
-# @return [void]
-# @see ORDERED_SERVICES
-def start_all_services
- ORDERED_SERVICES.map do |name|
- puts "Starting #{name}:"
- fork { exec("#{__FILE__} #{name}") }
- sleep(7)
- end
-end
+ORDERED_SERVICES = [:questions, :software, :storage, :users, :manager].freeze
dbus_server_manager = Agama::DBus::ServerManager.new
if ARGV.empty?
- start_all_services
- Signal.trap("SIGINT") do
- puts "Stopping all services..."
- exit
- end
- Process.wait
+ puts "ERROR: Using 'agamactl' to start all services no longer works."
+ puts "NOTE: It had race conditions all along and now there's a Rust service it can't reach too."
+ puts "NOTE: Use `systemctl start agama.service` instead"
+ puts "NOTE: which is setup by a) RPMs b) ./setup-service.sh"
+ exit 1
elsif ["-h", "--help"].include?(ARGV[0])
me = $PROGRAM_NAME
puts "Usage:"
diff --git a/service/lib/agama/dbus/clients/language.rb b/service/lib/agama/dbus/clients/locale.rb
similarity index 58%
rename from service/lib/agama/dbus/clients/language.rb
rename to service/lib/agama/dbus/clients/locale.rb
index 35a28515e4..ff27aaf19c 100644
--- a/service/lib/agama/dbus/clients/language.rb
+++ b/service/lib/agama/dbus/clients/locale.rb
@@ -24,54 +24,40 @@
module Agama
module DBus
module Clients
- # D-Bus client for language configuration
- class Language < Base
+ # D-Bus client for locale configuration
+ class Locale < Base
def initialize
super
- @dbus_object = service.object("/org/opensuse/Agama/Language1")
+ @dbus_object = service.object("/org/opensuse/Agama/Locale1")
@dbus_object.introspect
end
def service_name
- @service_name ||= "org.opensuse.Agama.Language1"
+ @service_name ||= "org.opensuse.Agama.Locale1"
end
- # Available languages for the installation
+ # Sets the supported locales. It can differs per product.
#
- # @return [Array>] id and name of each language
- def available_languages
- dbus_object["org.opensuse.Agama.Language1"]["AvailableLanguages"].map { |l| l[0..1] }
- end
-
- # Languages selected to install
- #
- # @return [Array] ids of the languages
- def selected_languages
- dbus_object["org.opensuse.Agama.Language1"]["MarkedForInstall"]
- end
-
- # Selects the languages to install
- #
- # @param ids [Array]
- def select_languages(ids)
- dbus_object.ToInstall(ids)
+ # @param locales [Array]
+ def supported_locales=(locales)
+ dbus_object.supported_locales = locales
end
# Finishes the language installation
def finish
- dbus_object.Finish
+ dbus_object.Commit
end
- # Registers a callback to run when the language changes
+ # Registers a callback to run when the selected locales changes
#
# @note Signal subscription is done only once. Otherwise, the latest subscription overrides
# the previous one.
#
- # @param block [Proc] Callback to run when a language is selected
+ # @param block [Proc] Callback to run when a locales are selected
def on_language_selected(&block)
on_properties_change(dbus_object) do |_, changes, _|
- languages = changes["MarkedForInstall"]
+ languages = changes["Locales"]
block.call(languages)
end
end
diff --git a/service/lib/agama/dbus/software/manager.rb b/service/lib/agama/dbus/software/manager.rb
index d290f453db..eac0ff69c9 100644
--- a/service/lib/agama/dbus/software/manager.rb
+++ b/service/lib/agama/dbus/software/manager.rb
@@ -22,7 +22,7 @@
require "dbus"
require "agama/dbus/base_object"
require "agama/dbus/with_service_status"
-require "agama/dbus/clients/language"
+require "agama/dbus/clients/locale"
require "agama/dbus/clients/network"
require "agama/dbus/interfaces/progress"
require "agama/dbus/interfaces/service_status"
@@ -135,7 +135,7 @@ def finish
# Registers callback to be called
def register_callbacks
- lang_client = Agama::DBus::Clients::Language.new
+ lang_client = Agama::DBus::Clients::Locale.new
lang_client.on_language_selected do |language_ids|
backend.languages = language_ids
end
diff --git a/service/lib/agama/manager.rb b/service/lib/agama/manager.rb
index 040724b103..2b214fabaa 100644
--- a/service/lib/agama/manager.rb
+++ b/service/lib/agama/manager.rb
@@ -25,7 +25,7 @@
require "agama/with_progress"
require "agama/installation_phase"
require "agama/service_status_recorder"
-require "agama/dbus/clients/language"
+require "agama/dbus/clients/locale"
require "agama/dbus/clients/software"
require "agama/dbus/clients/storage"
require "agama/dbus/clients/users"
@@ -124,9 +124,9 @@ def software
# Language manager
#
- # @return [DBus::Clients::Language]
+ # @return [DBus::Clients::Locale]
def language
- @language ||= DBus::Clients::Language.new
+ @language ||= DBus::Clients::Locale.new
end
# Users client
diff --git a/service/share/dbus.conf b/service/share/dbus.conf
index da06b7e009..47a6ef1f4c 100644
--- a/service/share/dbus.conf
+++ b/service/share/dbus.conf
@@ -36,7 +36,7 @@
-
+
@@ -44,7 +44,7 @@
-
+
diff --git a/service/test/agama/dbus/clients/language_test.rb b/service/test/agama/dbus/clients/locale_test.rb
similarity index 57%
rename from service/test/agama/dbus/clients/language_test.rb
rename to service/test/agama/dbus/clients/locale_test.rb
index 93e469d180..21b47bcdce 100644
--- a/service/test/agama/dbus/clients/language_test.rb
+++ b/service/test/agama/dbus/clients/locale_test.rb
@@ -20,17 +20,17 @@
# find current contact information at www.suse.com.
require_relative "../../../test_helper"
-require "agama/dbus/clients/language"
+require "agama/dbus/clients/locale"
require "dbus"
-describe Agama::DBus::Clients::Language do
+describe Agama::DBus::Clients::Locale do
before do
allow(Agama::DBus::Bus).to receive(:current).and_return(bus)
- allow(bus).to receive(:service).with("org.opensuse.Agama.Language1").and_return(service)
- allow(service).to receive(:object).with("/org/opensuse/Agama/Language1")
+ allow(bus).to receive(:service).with("org.opensuse.Agama.Locale1").and_return(service)
+ allow(service).to receive(:object).with("/org/opensuse/Agama/Locale1")
.and_return(dbus_object)
allow(dbus_object).to receive(:introspect)
- allow(dbus_object).to receive(:[]).with("org.opensuse.Agama.Language1")
+ allow(dbus_object).to receive(:[]).with("org.opensuse.Agama.Locale1")
.and_return(lang_iface)
end
@@ -41,44 +41,13 @@
subject { described_class.new }
- describe "#available_languages" do
- before do
- allow(lang_iface).to receive(:[]).with("AvailableLanguages").and_return(
- [
- ["en_US", "English (US)", {}],
- ["en_GB", "English (UK)", {}],
- ["es_ES", "Español", {}]
- ]
- )
- end
-
- it "returns the id and name for all available languages" do
- expect(subject.available_languages).to contain_exactly(
- ["en_US", "English (US)"],
- ["en_GB", "English (UK)"],
- ["es_ES", "Español"]
- )
- end
- end
-
- describe "#selected_languages" do
- before do
- allow(lang_iface).to receive(:[]).with("MarkedForInstall").and_return(["en_US", "es_ES"])
- end
-
- it "returns the name of the selected languages" do
- expect(subject.selected_languages).to contain_exactly("en_US", "es_ES")
- end
- end
-
- describe "#select_languages" do
+ describe "#supported_locales=" do
# Using partial double because methods are dynamically added to the proxy object
let(:dbus_object) { double(::DBus::ProxyObject) }
- it "selects the given languages" do
- expect(dbus_object).to receive(:ToInstall).with(["en_GB"])
-
- subject.select_languages(["en_GB"])
+ it "calls the D-Bus object" do
+ expect(dbus_object).to receive(:supported_locales=).with(["no", "se"])
+ subject.supported_locales = ["no", "se"]
end
end
@@ -86,7 +55,7 @@
let(:dbus_object) { double(::DBus::ProxyObject) }
it "calls the D-Bus finish method" do
- expect(dbus_object).to receive(:Finish)
+ expect(dbus_object).to receive(:Commit)
subject.finish
end
end
diff --git a/service/test/agama/manager_test.rb b/service/test/agama/manager_test.rb
index 96d78aeada..cf57345473 100644
--- a/service/test/agama/manager_test.rb
+++ b/service/test/agama/manager_test.rb
@@ -48,7 +48,7 @@
write: nil, on_service_status_change: nil, valid?: true
)
end
- let(:language) { instance_double(Agama::DBus::Clients::Language, finish: nil) }
+ let(:locale) { instance_double(Agama::DBus::Clients::Locale, finish: nil) }
let(:network) { instance_double(Agama::Network, install: nil) }
let(:storage) do
instance_double(
@@ -61,7 +61,7 @@
before do
allow(Agama::Network).to receive(:new).and_return(network)
- allow(Agama::DBus::Clients::Language).to receive(:new).and_return(language)
+ allow(Agama::DBus::Clients::Locale).to receive(:new).and_return(locale)
allow(Agama::DBus::Clients::Software).to receive(:new).and_return(software)
allow(Agama::DBus::Clients::Storage).to receive(:new).and_return(storage)
allow(Agama::DBus::Clients::Users).to receive(:new).and_return(users)
@@ -119,7 +119,7 @@
expect(network).to receive(:install)
expect(software).to receive(:install)
expect(software).to receive(:finish)
- expect(language).to receive(:finish)
+ expect(locale).to receive(:finish)
expect(storage).to receive(:install)
expect(storage).to receive(:finish)
expect(users).to receive(:write)
diff --git a/setup-service.sh b/setup-service.sh
index 52417f1b15..d418919279 100755
--- a/setup-service.sh
+++ b/setup-service.sh
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/bin/sh -x
# Using a git checkout in the current directory,
# set up the service (backend) part of agama
@@ -32,13 +32,29 @@ sudosed() {
sed -e "$1" "$2" | $SUDO tee "$3" > /dev/null
}
-# - Install the service dependencies
+# - Install RPM dependencies
+
+# this repo can be removed once python-language-data reaches Factory
+test -f /etc/zypp/repos.d/d_l_python.repo || \
+ $SUDO zypper --non-interactive \
+ addrepo https://download.opensuse.org/repositories/devel:/languages:/python/openSUSE_Tumbleweed/ d_l_python
+$SUDO zypper --non-interactive --gpg-auto-import-keys install gcc gcc-c++ make openssl-devel ruby-devel \
+ python-langtable-data \
+ git augeas-devel jemalloc-devel || exit 1
+
+# - Install service rubygem dependencies
(
cd $MYDIR/service
bundle config set --local path 'vendor/bundle'
bundle install
)
+# - build also rust service
+(
+ cd $MYDIR/rust
+ cargo build
+)
+
# - D-Bus configuration
$SUDO cp -v $MYDIR/service/share/dbus.conf /usr/share/dbus-1/agama.conf
@@ -54,9 +70,25 @@ $SUDO cp -v $MYDIR/service/share/dbus.conf /usr/share/dbus-1/agama.conf
done
sudosed "s@\(ExecStart\)=/usr/bin/@\1=$MYDIR/service/bin/@" \
systemd.service /usr/lib/systemd/system/agama.service
+)
+
+# and same for rust service
+(
+ cd $MYDIR/rust/share
+ DBUSDIR=/usr/share/dbus-1/agama-services
+ for SVC in org.opensuse.Agama*.service; do
+ # it is intention to use debug here to get more useful debugging output
+ sudosed "s@\(Exec\)=/usr/bin/@\1=$MYDIR/rust/target/debug/@" $SVC $DBUSDIR/$SVC
+ done
+)
+
+# systemd reload and start of service
+(
$SUDO systemctl daemon-reload
# Start the separate dbus-daemon for Agama
- $SUDO systemctl start agama.service
+ # (in CI we run a custom cockpit-ws which replaces the cockpit.socket
+ # dependency, continue in that case)
+ $SUDO systemctl start agama.service || pgrep cockpit-ws
)
# - Make sure NetworkManager is running
diff --git a/setup.sh b/setup.sh
index 00d47f9b19..a4e2c48f8d 100755
--- a/setup.sh
+++ b/setup.sh
@@ -18,15 +18,15 @@ else
SUDO=""
fi
-# Install dependencies
-
-$SUDO zypper --non-interactive install gcc gcc-c++ make openssl-devel ruby-devel \
- 'npm>=18' git augeas-devel cockpit jemalloc-devel || exit 1
-
# Backend setup
$MYDIR/setup-service.sh
+# Install Frontend dependencies
+
+$SUDO zypper --non-interactive --gpg-auto-import-keys install \
+ make git 'npm>=18' cockpit || exit 1
+
# Web Frontend
$SUDO systemctl start cockpit
diff --git a/web/cspell.json b/web/cspell.json
index dec371fe84..c98e662255 100644
--- a/web/cspell.json
+++ b/web/cspell.json
@@ -25,6 +25,7 @@
"dasd",
"dasds",
"dbus",
+ "España",
"filecontent",
"filename",
"fullname",
diff --git a/web/src/client/language.js b/web/src/client/language.js
index c486c27d43..d8a4e47a6f 100644
--- a/web/src/client/language.js
+++ b/web/src/client/language.js
@@ -22,9 +22,9 @@
// @ts-check
import DBusClient from "./dbus";
-const LANGUAGE_SERVICE = "org.opensuse.Agama.Language1";
-const LANGUAGE_IFACE = "org.opensuse.Agama.Language1";
-const LANGUAGE_PATH = "/org/opensuse/Agama/Language1";
+const LANGUAGE_SERVICE = "org.opensuse.Agama.Locale1";
+const LANGUAGE_IFACE = "org.opensuse.Agama.Locale1";
+const LANGUAGE_PATH = "/org/opensuse/Agama/Locale1";
/**
* @typedef {object} Language
@@ -50,9 +50,12 @@ class LanguageClient {
*/
async getLanguages() {
const proxy = await this.client.proxy(LANGUAGE_IFACE);
- return proxy.AvailableLanguages.map(lang => {
- const [id, name] = lang;
- return { id, name };
+ const locales = proxy.SupportedLocales;
+ const labels = await proxy.LabelsForLocales();
+ return locales.map((locale, index) => {
+ // labels structure is [[en_lang, en_territory], [native_lang, native_territory]]
+ const [[en_lang,], [,]] = labels[index];
+ return { id: locale, name: en_lang };
});
}
@@ -63,7 +66,7 @@ class LanguageClient {
*/
async getSelectedLanguages() {
const proxy = await this.client.proxy(LANGUAGE_IFACE);
- return proxy.MarkedForInstall;
+ return proxy.Locales;
}
/**
@@ -74,7 +77,7 @@ class LanguageClient {
*/
async setLanguages(langIDs) {
const proxy = await this.client.proxy(LANGUAGE_IFACE);
- return proxy.ToInstall(langIDs);
+ proxy.Locales = langIDs;
}
/**
@@ -85,7 +88,7 @@ class LanguageClient {
*/
onLanguageChange(handler) {
return this.client.onObjectChanged(LANGUAGE_PATH, LANGUAGE_IFACE, changes => {
- const selected = changes.MarkedForInstall.v[0];
+ const selected = changes.Locales.v[0];
handler(selected);
});
}
diff --git a/web/src/client/language.test.js b/web/src/client/language.test.js
index e23399ed65..ebe05fb712 100644
--- a/web/src/client/language.test.js
+++ b/web/src/client/language.test.js
@@ -29,9 +29,10 @@ jest.mock("./dbus");
const langProxy = {
wait: jest.fn(),
- AvailableLanguages: [
- ["cs_CZ", "Cestina", {}]
- ]
+ SupportedLocales: ["es_ES.UTF-8", "en_US.UTF-8"],
+ LabelsForLocales: jest.fn().mockResolvedValue(
+ [[["Spanish", "Spain"], ["Español", "España"]], [['English', 'United States'], ['English', 'United States']]]
+ ),
};
jest.mock("./dbus");
@@ -47,6 +48,9 @@ describe("#getLanguages", () => {
it("returns the list of available languages", async () => {
const client = new LanguageClient();
const availableLanguages = await client.getLanguages();
- expect(availableLanguages).toEqual([{ id: "cs_CZ", name: "Cestina" }]);
+ expect(availableLanguages).toEqual([
+ { id: "es_ES.UTF-8", name: "Spanish" },
+ { id: "en_US.UTF-8", name: "English" }
+ ]);
});
});