Skip to content

Commit 44e1eb1

Browse files
authored
profiles: Add desktop entries for workloads (#63)
Instead of relying on users to create and map their own desktop entries, qubesome auto generate them based on all the workloads configured for the given profile. Note that a generic qubesome icon is used at present. In the future, a better approach will be defined to support workloads providing their own icons.
2 parents 073a71d + af8776e commit 44e1eb1

File tree

4 files changed

+184
-11
lines changed

4 files changed

+184
-11
lines changed

.github/workflows/scorecard.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: Scorecard supply-chain security
2+
on:
3+
# For Branch-Protection check. Only the default branch is supported. See
4+
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
5+
branch_protection_rule:
6+
# To guarantee Maintained check is occasionally updated. See
7+
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
8+
schedule:
9+
- cron: '0 6 * * 0'
10+
push:
11+
branches: [ "main" ]
12+
13+
# Declare default permissions as read only.
14+
permissions: read-all
15+
16+
jobs:
17+
analysis:
18+
name: Scorecard analysis
19+
runs-on: ubuntu-latest
20+
permissions:
21+
# Needed to upload the results to code-scanning dashboard.
22+
security-events: write
23+
# Needed to publish results and get a badge (see publish_results below).
24+
id-token: write
25+
26+
steps:
27+
- name: "Checkout code"
28+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
29+
with:
30+
persist-credentials: false
31+
32+
- name: "Run analysis"
33+
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
34+
with:
35+
results_file: results.sarif
36+
results_format: sarif
37+
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
38+
# - you want to enable the Branch-Protection check on a *public* repository
39+
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional.
40+
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
41+
42+
# Public repositories:
43+
# - Publish results to OpenSSF REST API for easy access by consumers
44+
# - Allows the repository to include the Scorecard badge.
45+
# - See https://github.com/ossf/scorecard-action#publishing-results.
46+
publish_results: true
47+
48+
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
49+
# format to the repository Actions tab.
50+
- name: "Upload artifact"
51+
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
52+
with:
53+
name: SARIF file
54+
path: results.sarif
55+
retention-days: 5
56+
57+
# Upload the results to GitHub's code scanning dashboard (optional).
58+
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
59+
- name: "Upload to code-scanning"
60+
uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
61+
with:
62+
sarif_file: results.sarif

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## qubesome
1+
## qubesome [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/qubesome/cli/badge)](https://scorecard.dev/viewer/?uri=github.com/qubesome/cli)
22

33
Welcome to qubesome! This project is a command-line interface (CLI) tool aimed
44
to simplify managing Linux desktop configurations. It works by virtualizing

internal/images/images.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,16 +122,19 @@ func PullImageIfNotPresent(bin, image string) error {
122122
return PullImage(bin, image)
123123
}
124124

125-
func imagePresent(bin, image string) (bool, error) {
126-
slog.Debug("checking if container image is present", "image", image)
125+
func imagePresent(bin, image string) (found bool, err error) {
126+
defer func() {
127+
slog.Debug("checking container image presence", "image", image, "found", found)
128+
}()
127129
cmd := execabs.Command(bin, "images", "-q", image)
128130

129131
out, err := cmd.Output()
130132
if len(out) > 0 && err == nil {
131-
return true, nil
133+
found = true
134+
return
132135
}
133136

134-
return false, err
137+
return
135138
}
136139

137140
func MissingImages(bin string, cfg *types.Config) ([]string, error) {

internal/profiles/profiles.go

Lines changed: 114 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strconv"
1111
"strings"
1212
"sync"
13+
"text/template"
1314
"time"
1415

1516
securejoin "github.com/cyphar/filepath-securejoin"
@@ -34,11 +35,23 @@ import (
3435
"github.com/qubesome/cli/pkg/inception"
3536
"golang.org/x/sys/execabs"
3637
"golang.org/x/term"
38+
"gopkg.in/yaml.v3"
3739
)
3840

3941
var (
4042
ContainerNameFormat = "qubesome-%s"
4143
defaultProfileImage = "ghcr.io/qubesome/xorg:latest"
44+
45+
appTemplate = `[Desktop Entry]
46+
Version=1.0
47+
Name={{.Name}}
48+
Exec=/bin/sh -c "/usr/local/bin/qubesome run {{.Name}} %U"
49+
Icon=qubesome-generic
50+
StartupNotify=true
51+
Terminal=false
52+
Type=Application
53+
`
54+
qubesomeIconBlack = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="2000" height="1250" viewBox="0 0 2000 1250"><g transform="matrix(1,0,0,1,0,0)"><svg viewBox="0 0 512 320" data-background-color="#ffffff" preserveAspectRatio="xMidYMid meet" height="1250" width="2000" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="tight-bounds" transform="matrix(1,0,0,1,0,0)"><svg viewBox="0 0 512 320" height="320" width="512"><g><svg></svg></g><g><svg viewBox="0 0 512 320" height="320" width="512"><g><path transform="translate(256,160) scale(226.27417,226.27417)" d="M-0.707 0.707l0-1.414 1.414 0 0 1.414z" fill="#000000" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal" data-fill-palette-color="tertiary"></path></g><g transform="matrix(1,0,0,1,140.8,131.0465864758607)"><svg viewBox="0 0 230.4 57.90682704827861" height="57.90682704827861" width="230.4"><g><svg viewBox="0 0 313.6537803795311 78.83114241960186" height="57.90682704827861" width="230.4"><g transform="matrix(1,0,0,1,83.25378037953108,10.894897396630942)"><svg viewBox="0 0 230.40000000000003 57.04134762633998" height="57.04134762633998" width="230.40000000000003"><g id="textblocktransform"><svg viewBox="0 0 230.40000000000003 57.04134762633998" height="57.04134762633998" width="230.40000000000003" id="textblock"><g><svg viewBox="0 0 230.40000000000003 57.04134762633998" height="57.04134762633998" width="230.40000000000003"><g transform="matrix(1,0,0,1,0,0)"><svg width="230.40000000000003" viewBox="1.7 -37.65 195.88000000000002 48.5" height="57.04134762633998" data-palette-color="#ffffff"><path d="M16.6-26.45L21.95-26.45 21.95 10.85 15.8 10.15 15.8-2.65Q13.7 0.75 10.1 0.75L10.1 0.75Q6.1 0.75 3.9-2.9 1.7-6.55 1.7-13.2L1.7-13.2Q1.7-17.45 2.83-20.6 3.95-23.75 5.95-25.45 7.95-27.15 10.6-27.15L10.6-27.15Q14.05-27.15 16.3-23.95L16.3-23.95 16.6-26.45ZM11.85-3.9Q13.05-3.9 14-4.7 14.95-5.5 15.8-7.1L15.8-7.1 15.8-19.8Q14.1-22.55 12.1-22.55L12.1-22.55Q10.3-22.55 9.2-20.33 8.1-18.1 8.1-13.25L8.1-13.25Q8.1-8.1 9.08-6 10.05-3.9 11.85-3.9L11.85-3.9ZM45.8-26.45L45.8 0 40.45 0 40.15-3.35Q38.9-1.3 37.35-0.28 35.8 0.75 33.75 0.75L33.75 0.75Q30.85 0.75 29.17-1.3 27.5-3.35 27.5-6.65L27.5-6.65 27.5-26.45 33.65-26.45 33.65-7Q33.65-3.9 35.75-3.9L35.75-3.9Q36.95-3.9 37.9-4.83 38.85-5.75 39.65-7.45L39.65-7.45 39.65-26.45 45.8-26.45ZM63.65-27.15Q67.5-27.15 69.55-23.65 71.59-20.15 71.59-13.25L71.59-13.25Q71.59-6.75 69.3-3 67 0.75 63 0.75L63 0.75Q61.25 0.75 59.8-0.13 58.35-1 57.3-2.55L57.3-2.55 56.95 0 51.5 0 51.5-37 57.65-37.65 57.65-23.6Q58.7-25.3 60.22-26.23 61.75-27.15 63.65-27.15L63.65-27.15ZM61.35-3.9Q63.2-3.9 64.22-6 65.25-8.1 65.25-13.25L65.25-13.25Q65.25-18.6 64.32-20.58 63.4-22.55 61.7-22.55L61.7-22.55Q60.55-22.55 59.5-21.6 58.45-20.65 57.65-19.1L57.65-19.1 57.65-6.7Q58.3-5.45 59.27-4.67 60.25-3.9 61.35-3.9L61.35-3.9ZM95.04-13.85Q95.04-13.4 94.89-11.3L94.89-11.3 81.24-11.3Q81.44-7.2 82.67-5.58 83.89-3.95 86.14-3.95L86.14-3.95Q87.69-3.95 88.94-4.48 90.19-5 91.64-6.15L91.64-6.15 94.19-2.65Q90.69 0.75 85.79 0.75L85.79 0.75Q80.59 0.75 77.82-2.85 75.04-6.45 75.04-13L75.04-13Q75.04-19.55 77.69-23.35 80.34-27.15 85.19-27.15L85.19-27.15Q89.84-27.15 92.44-23.78 95.04-20.4 95.04-13.85L95.04-13.85ZM89.04-15.3L89.04-15.65Q89.04-19.4 88.14-21.13 87.24-22.85 85.24-22.85L85.24-22.85Q83.39-22.85 82.42-21.18 81.44-19.5 81.24-15.3L81.24-15.3 89.04-15.3ZM106.69-27.15Q111.39-27.15 114.64-24.05L114.64-24.05 112.29-20.7Q110.89-21.7 109.64-22.2 108.39-22.7 107.09-22.7L107.09-22.7Q105.64-22.7 104.82-21.93 103.99-21.15 103.99-19.8L103.99-19.8Q103.99-18.45 104.92-17.63 105.84-16.8 108.64-15.7L108.64-15.7Q112.09-14.35 113.74-12.5 115.39-10.65 115.39-7.55L115.39-7.55Q115.39-3.7 112.69-1.48 109.99 0.75 105.84 0.75L105.84 0.75Q103.14 0.75 100.87-0.23 98.59-1.2 96.89-2.95L96.89-2.95 99.89-6.25Q102.69-3.8 105.59-3.8L105.59-3.8Q107.24-3.8 108.19-4.67 109.14-5.55 109.14-7.1L109.14-7.1Q109.14-8.25 108.74-8.97 108.34-9.7 107.34-10.33 106.34-10.95 104.34-11.7L104.34-11.7Q100.89-13.05 99.42-14.85 97.94-16.65 97.94-19.45L97.94-19.45Q97.94-22.8 100.32-24.98 102.69-27.15 106.69-27.15L106.69-27.15ZM128.64-27.15Q133.64-27.15 136.41-23.65 139.19-20.15 139.19-13.25L139.19-13.25Q139.19-6.65 136.39-2.95 133.59 0.75 128.64 0.75L128.64 0.75Q123.69 0.75 120.89-2.88 118.09-6.5 118.09-13.25L118.09-13.25Q118.09-19.95 120.89-23.55 123.69-27.15 128.64-27.15L128.64-27.15ZM128.64-22.5Q126.49-22.5 125.49-20.38 124.49-18.25 124.49-13.25L124.49-13.25Q124.49-8.2 125.49-6.08 126.49-3.95 128.64-3.95L128.64-3.95Q130.79-3.95 131.79-6.08 132.79-8.2 132.79-13.25L132.79-13.25Q132.79-18.3 131.79-20.4 130.79-22.5 128.64-22.5L128.64-22.5ZM167.39-27.15Q170.04-27.15 171.64-25.1 173.24-23.05 173.24-19.6L173.24-19.6 173.24 0 167.19 0 167.19-18.95Q167.19-20.85 166.69-21.68 166.19-22.5 165.24-22.5L165.24-22.5Q163.29-22.5 161.49-18.8L161.49-18.8 161.49 0 155.49 0 155.49-18.95Q155.49-22.5 153.59-22.5L153.59-22.5Q151.54-22.5 149.79-18.8L149.79-18.8 149.79 0 143.74 0 143.74-26.45 149.09-26.45 149.49-23.05Q152.09-27.15 155.74-27.15L155.74-27.15Q157.59-27.15 158.94-26 160.29-24.85 160.99-22.75L160.99-22.75Q163.64-27.15 167.39-27.15L167.39-27.15ZM197.58-13.85Q197.58-13.4 197.43-11.3L197.43-11.3 183.78-11.3Q183.98-7.2 185.21-5.58 186.43-3.95 188.68-3.95L188.68-3.95Q190.23-3.95 191.48-4.48 192.73-5 194.18-6.15L194.18-6.15 196.73-2.65Q193.23 0.75 188.33 0.75L188.33 0.75Q183.13 0.75 180.36-2.85 177.58-6.45 177.58-13L177.58-13Q177.58-19.55 180.23-23.35 182.88-27.15 187.73-27.15L187.73-27.15Q192.38-27.15 194.98-23.78 197.58-20.4 197.58-13.85L197.58-13.85ZM191.58-15.3L191.58-15.65Q191.58-19.4 190.68-21.13 189.78-22.85 187.78-22.85L187.78-22.85Q185.93-22.85 184.96-21.18 183.98-19.5 183.78-15.3L183.78-15.3 191.58-15.3Z" opacity="1" transform="matrix(1,0,0,1,0,0)" fill="#ffffff" class="wordmark-text-0" data-fill-palette-color="quaternary" id="text-0"></path></svg></g></svg></g></svg></g></svg></g><g><svg viewBox="0 0 69.78768719729524 78.83114241960186" height="78.83114241960186" width="69.78768719729524"><g><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" version="1.1" style="shape-rendering:geometricPrecision;text-rendering:geometricPrecision;image-rendering:optimizeQuality;" viewBox="19 15.25 61 68.90470053837926" x="0" y="0" fill-rule="evenodd" clip-rule="evenodd" height="78.83114241960186" width="69.78768719729524" class="icon-icon-0" data-fill-palette-color="quaternary" id="icon-0"><g fill="#ffffff" data-fill-palette-color="quaternary"><path class="" d="M53 16C62 21 69 25 77 30 79 31 80 32 80 35V64C80 66 79 68 77 69 68 74 62 78 53 83 50 84 49 85 46 83 37 78 30 74 22 69 20 68 19 67 19 64V35C19 33 20 32 22 30 31 25 38 21 47 16 49 15 51 15 53 16M52 51V78C60 73 68 69 75 64V37zM48 78V51L25 37V64C33 69 41 73 48 78M27 34L50 48 73 34C65 29 57 25 50 20 42 25 34 29 27 34" fill="#ffffff" fill-rule="nonzero" data-fill-palette-color="quaternary"></path></g></svg></g></svg></g></svg></g></svg></g></svg></g><defs></defs></svg><rect width="512" height="320" fill="none" stroke="none" visibility="hidden"></rect></g></svg></g></svg>`
4255
)
4356

4457
func Run(opts ...command.Option[Options]) error {
@@ -321,7 +334,7 @@ func Start(runner string, profile *types.Profile, cfg *types.Config, interactive
321334

322335
err = createNewDisplay(binary,
323336
creds.CA, creds.ClientPEM, creds.ClientKeyPEM,
324-
profile, strconv.Itoa(int(profile.Display)), interactive)
337+
profile, strconv.Itoa(int(profile.Display)), interactive, cfg)
325338
if err != nil {
326339
return err
327340
}
@@ -425,7 +438,7 @@ func startWindowManager(bin, name, display, wm string) error {
425438
return nil
426439
}
427440

428-
func createNewDisplay(bin string, ca, cert, key []byte, profile *types.Profile, display string, interactive bool) error {
441+
func createNewDisplay(bin string, ca, cert, key []byte, profile *types.Profile, display string, interactive bool, cfg *types.Config) error {
429442
command := "Xephyr"
430443
res, err := resolution.Primary()
431444
if err != nil {
@@ -603,7 +616,7 @@ func createNewDisplay(bin string, ca, cert, key []byte, profile *types.Profile,
603616
return err
604617
}
605618

606-
err = setupAppsDir(profile)
619+
err = setupAppsDir(profile, cfg)
607620
if err != nil {
608621
return err
609622
}
@@ -726,17 +739,31 @@ func setupRunUserDir(dir string) error {
726739
return nil
727740
}
728741

729-
func setupAppsDir(profile *types.Profile) error {
742+
func setupAppsDir(profile *types.Profile, cfg *types.Config) error {
730743
dir := files.ProfileDir(profile.Name)
731-
err := os.MkdirAll(filepath.Join(dir, "applications"), files.DirMode)
744+
745+
appsDir := filepath.Join(dir, "applications")
746+
err := os.MkdirAll(appsDir, files.DirMode)
732747
if err != nil {
733748
return fmt.Errorf("failed to create <profile>/applications dir: %w", err)
734749
}
735-
err = os.MkdirAll(filepath.Join(dir, "icons"), files.DirMode)
750+
751+
appsRoot, err := os.OpenRoot(appsDir)
752+
if err != nil {
753+
return err
754+
}
755+
756+
iconsDir := filepath.Join(dir, "icons")
757+
err = os.MkdirAll(iconsDir, files.DirMode)
736758
if err != nil {
737759
return fmt.Errorf("failed to create <profile>/icons dir: %w", err)
738760
}
739761

762+
iconsRoot, err := os.OpenRoot(iconsDir)
763+
if err != nil {
764+
return err
765+
}
766+
740767
for _, name := range profile.Flatpaks {
741768
src := filepath.Join(files.FlatpakApps(), name+".desktop")
742769
target := filepath.Join(dir, "applications", name+".desktop")
@@ -756,6 +783,87 @@ func setupAppsDir(profile *types.Profile) error {
756783

757784
slog.Debug("added flatpak workload", "name", name)
758785
}
786+
787+
return hydrateApps(cfg, appsRoot, iconsRoot)
788+
}
789+
790+
func createGenericIcon(root *os.Root) error {
791+
f, err := root.Create("qubesome-generic.svg")
792+
if err != nil {
793+
return err
794+
}
795+
defer f.Close()
796+
797+
_, err = f.WriteString(qubesomeIconBlack)
798+
return err
799+
}
800+
801+
func hydrateApps(cfg *types.Config, appsRoot, iconsRoot *os.Root) error {
802+
slog.Debug("hydrating profile workloads")
803+
wf, err := cfg.WorkloadFiles()
804+
if err != nil {
805+
return fmt.Errorf("cannot get workloads files: %w", err)
806+
}
807+
808+
if len(wf) == 0 {
809+
return nil
810+
}
811+
812+
if err = createGenericIcon(iconsRoot); err != nil {
813+
slog.Debug("failed to create generic icon", "error", err)
814+
}
815+
816+
for _, fn := range wf {
817+
slog.Debug("profile workload", "filename", fn)
818+
fi, err := os.Stat(fn)
819+
if err != nil {
820+
slog.Error("cannot stat file", "file", fn, "error", err)
821+
}
822+
823+
if !fi.Mode().IsRegular() {
824+
continue
825+
}
826+
827+
data, err := os.ReadFile(fn)
828+
if err != nil {
829+
slog.Error("cannot read file", "filename", fn, "error", err)
830+
continue
831+
}
832+
833+
w := types.Workload{}
834+
err = yaml.Unmarshal(data, &w)
835+
if err != nil {
836+
slog.Error("cannot unmarshal workload file", "filename", fn, "error", err)
837+
continue
838+
}
839+
840+
w.Name = filepath.Base(strings.TrimSuffix(fn, filepath.Ext(fn)))
841+
842+
if err = w.Validate(); err != nil {
843+
slog.Error("invalid workload", "error", err)
844+
continue
845+
}
846+
847+
appFile := w.Name + ".desktop"
848+
f, err := appsRoot.Create(appFile)
849+
if err != nil {
850+
slog.Error("cannot create file", "filename", appFile, "error", err)
851+
continue
852+
}
853+
854+
tmpl, err := template.New("app").Parse(appTemplate)
855+
if err != nil {
856+
f.Close()
857+
858+
slog.Error("cannot create template", "error", err)
859+
continue
860+
}
861+
err = tmpl.Execute(f, w)
862+
if err != nil {
863+
slog.Error("cannot execute template", "error", err)
864+
}
865+
f.Close()
866+
}
759867
return nil
760868
}
761869

0 commit comments

Comments
 (0)