Skip to content

Commit 409deb4

Browse files
3v1n0olivercalder
andauthored
snapenv: Include snap application environment state (#16016)
* snapenv: Include snap application environment state When running an application of a snap is complicated to get some per-app state values such as its name, desktop file and common IDs, but these are actually important to make the application to be able to identify itself properly in a desktop environment. As per this, expose the application information as env variables. Related to: https://bugs.launchpad.net/ubuntu/+source/snapd/+bug/2125222 * tests: add snap env check for app with common id and desktop file Signed-off-by: Oliver Calder <oliver.calder@canonical.com> --------- Signed-off-by: Oliver Calder <oliver.calder@canonical.com> Co-authored-by: Oliver Calder <oliver.calder@canonical.com>
1 parent 46c2015 commit 409deb4

4 files changed

Lines changed: 143 additions & 15 deletions

File tree

cmd/snap/cmd_run.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1679,7 +1679,7 @@ func (x *cmdRun) runSnapConfine(info *snap.Info, runner runnable, beforeExec fun
16791679
return err
16801680
}
16811681

1682-
snapenv.ExtendEnvForRun(env, info, runner.Component(), opts)
1682+
snapenv.ExtendEnvForRun(env, info, runner.App(), runner.Component(), opts)
16831683

16841684
if len(xauthPath) > 0 {
16851685
// Environment is not nil here because it comes from

snap/snapenv/snapenv.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,25 @@ var userCurrent = user.Current
4949
//
5050
// It ensures all SNAP_* override any pre-existing environment
5151
// variables.
52-
func ExtendEnvForRun(env osutil.Environment, info *snap.Info, component *snap.ComponentInfo, opts *dirs.SnapDirOptions) {
52+
func ExtendEnvForRun(env osutil.Environment, info *snap.Info, app *snap.AppInfo, component *snap.ComponentInfo, opts *dirs.SnapDirOptions) {
5353
// Set various SNAP_ environment variables as well as some non-SNAP variables,
5454
// depending on snap confinement mode. Note that this does not include environment
5555
// set by snap-exec.
56-
for k, v := range snapEnv(info, component, opts) {
56+
for k, v := range snapEnv(info, app, component, opts) {
5757
env[k] = v
5858
}
5959
}
6060

61-
func snapEnv(info *snap.Info, component *snap.ComponentInfo, opts *dirs.SnapDirOptions) osutil.Environment {
61+
func snapEnv(info *snap.Info, app *snap.AppInfo, component *snap.ComponentInfo, opts *dirs.SnapDirOptions) osutil.Environment {
6262
// Environment variables with basic properties of a snap.
6363
env := basicEnv(info)
6464

65+
if app != nil {
66+
for k, v := range appEnv(info, app) {
67+
env[k] = v
68+
}
69+
}
70+
6571
if component != nil {
6672
for k, v := range componentEnv(info, component) {
6773
env[k] = v
@@ -137,6 +143,26 @@ func basicEnv(info *snap.Info) osutil.Environment {
137143
logger.Noticef("cannot determine existence of save data directory for snap %q: %v",
138144
info.InstanceName(), err)
139145
}
146+
147+
return env
148+
}
149+
150+
// appEnv returns the app-level environment variables for a snap.
151+
func appEnv(info *snap.Info, app *snap.AppInfo) osutil.Environment {
152+
env := osutil.Environment{
153+
"SNAP_APP_NAME": app.Name,
154+
}
155+
156+
if app.CommonID != "" {
157+
env["SNAP_APP_COMMON_ID"] = app.CommonID
158+
}
159+
if df := app.DesktopFile(); df != "" && osutil.FileExists(df) {
160+
env["SNAP_APP_DESKTOP_FILE"] = df
161+
}
162+
if app.BusName != "" {
163+
env["SNAP_APP_BUS_NAME"] = app.BusName
164+
}
165+
140166
return env
141167
}
142168

snap/snapenv/snapenv_test.go

Lines changed: 91 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,35 @@ var mockSnapInfo = &snap.Info{
6363
Revision: snap.R(17),
6464
},
6565
}
66+
var mockSnapInfoWithDesktopFile = func() *snap.Info {
67+
mi := *mockSnapInfo
68+
mi.Plugs = map[string]*snap.PlugInfo{
69+
"desktop": {
70+
Snap: &mi,
71+
Interface: "desktop",
72+
Name: "desktop",
73+
Attrs: map[string]any{
74+
"desktop-file-ids": []any{"io.snapcraft.foo.bar.desktop"},
75+
},
76+
},
77+
}
78+
return &mi
79+
}()
80+
var mockAppInfo = &snap.AppInfo{
81+
Snap: mockSnapInfo,
82+
Name: "bar",
83+
CommonID: "io.snapcraft.foo.bar",
84+
BusName: "io.snapcraft.foo.bar.bus",
85+
}
86+
var mockAppInfoWithDesktopFile = func() *snap.AppInfo {
87+
mai := *mockAppInfo
88+
mai.Snap = mockSnapInfoWithDesktopFile
89+
return &mai
90+
}()
91+
var mockAppInfoMinimal = &snap.AppInfo{
92+
Snap: mockSnapInfo,
93+
Name: "bar",
94+
}
6695
var mockComponentInfo = &snap.ComponentInfo{
6796
Component: naming.ComponentRef{
6897
SnapName: "foo",
@@ -94,6 +123,10 @@ var mockClassicSnapInfo = &snap.Info{
94123
func (s *HTestSuite) SetUpTest(c *C) {
95124
s.BaseTest.SetUpTest(c)
96125
s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))
126+
127+
defaultDesktopFilesDir := dirs.SnapDesktopFilesDir
128+
dirs.SnapDesktopFilesDir = c.MkDir()
129+
s.BaseTest.AddCleanup(func() { dirs.SnapDesktopFilesDir = defaultDesktopFilesDir })
97130
}
98131

99132
func (s *HTestSuite) TearDownTest(c *C) {
@@ -160,6 +193,47 @@ func (ts *HTestSuite) TestBasicWithSources(c *C) {
160193
})
161194
}
162195

196+
func (ts *HTestSuite) TestAppEnvironment(c *C) {
197+
env := appEnv(mockSnapInfo, mockAppInfo)
198+
c.Assert(env, DeepEquals, osutil.Environment{
199+
"SNAP_APP_NAME": "bar",
200+
"SNAP_APP_COMMON_ID": "io.snapcraft.foo.bar",
201+
"SNAP_APP_BUS_NAME": "io.snapcraft.foo.bar.bus",
202+
})
203+
}
204+
205+
func (ts *HTestSuite) testAppDesktopFileEnvironment(c *C, appInfo *snap.AppInfo, desktopFileName string) {
206+
desktopFilePath := filepath.Join(dirs.SnapDesktopFilesDir, desktopFileName)
207+
desktopFile := fmt.Sprintf(`[Desktop Entry]
208+
Exec=%s
209+
X-SnapInstanceName=%s
210+
X-SnapAppName=%s
211+
`, appInfo.Command, appInfo.Snap.InstanceName(), appInfo.Name)
212+
c.Assert(os.WriteFile(desktopFilePath, []byte(desktopFile), 0644), IsNil)
213+
214+
c.Assert(appInfo.DesktopFile(), Equals, desktopFilePath)
215+
216+
env := appEnv(appInfo.Snap, appInfo)
217+
c.Assert(env, DeepEquals, osutil.Environment{
218+
"SNAP_APP_NAME": appInfo.Name,
219+
"SNAP_APP_COMMON_ID": appInfo.CommonID,
220+
"SNAP_APP_BUS_NAME": appInfo.BusName,
221+
"SNAP_APP_DESKTOP_FILE": appInfo.DesktopFile(),
222+
})
223+
}
224+
225+
func (ts *HTestSuite) TestAppWithFallbackDesktopFileIDEnvironment(c *C) {
226+
ts.testAppDesktopFileEnvironment(c, mockAppInfo, "foo_bar.desktop")
227+
}
228+
229+
func (ts *HTestSuite) TestAppWithDesktopFileIDEnvironment(c *C) {
230+
ts.testAppDesktopFileEnvironment(c, mockAppInfoWithDesktopFile, "io.snapcraft.foo.bar.desktop")
231+
}
232+
233+
func (ts *HTestSuite) TestAppWithDesktopFileIDUsingFallbackDesktopFileEnvironment(c *C) {
234+
ts.testAppDesktopFileEnvironment(c, mockAppInfoWithDesktopFile, "foo_bar.desktop")
235+
}
236+
163237
func (ts *HTestSuite) TestSaveDataEnvironmentNotPresent(c *C) {
164238
dirs.SetRootDir(c.MkDir())
165239
ts.AddCleanup(func() { dirs.SetRootDir("") })
@@ -236,7 +310,7 @@ func (s *HTestSuite) TestSnapRunSnapExecEnv(c *C) {
236310
os.Setenv("HOME", "")
237311
}
238312

239-
env := snapEnv(info, nil, nil)
313+
env := snapEnv(info, mockAppInfo, nil, nil)
240314
c.Assert(env, DeepEquals, osutil.Environment{
241315
"SNAP": fmt.Sprintf("%s/snapname/42", dirs.CoreSnapMountDir),
242316
"SNAP_COMMON": "/var/snap/snapname/common",
@@ -256,6 +330,9 @@ func (s *HTestSuite) TestSnapRunSnapExecEnv(c *C) {
256330
"SNAP_REAL_HOME": usr.HomeDir,
257331
"SNAP_UID": fmt.Sprint(sys.Getuid()),
258332
"SNAP_EUID": fmt.Sprint(sys.Geteuid()),
333+
"SNAP_APP_COMMON_ID": "io.snapcraft.foo.bar",
334+
"SNAP_APP_BUS_NAME": "io.snapcraft.foo.bar.bus",
335+
"SNAP_APP_NAME": "bar",
259336
})
260337
}
261338
}
@@ -279,7 +356,7 @@ func (s *HTestSuite) TestParallelInstallSnapRunSnapExecEnv(c *C) {
279356
os.Setenv("HOME", "")
280357
}
281358

282-
env := snapEnv(info, nil, nil)
359+
env := snapEnv(info, mockAppInfoMinimal, nil, nil)
283360
c.Check(env, DeepEquals, osutil.Environment{
284361
// Those are mapped to snap-specific directories by
285362
// mount namespace setup
@@ -303,6 +380,7 @@ func (s *HTestSuite) TestParallelInstallSnapRunSnapExecEnv(c *C) {
303380
"SNAP_REAL_HOME": usr.HomeDir,
304381
"SNAP_UID": fmt.Sprint(sys.Getuid()),
305382
"SNAP_EUID": fmt.Sprint(sys.Geteuid()),
383+
"SNAP_APP_NAME": "bar",
306384
})
307385
}
308386
}
@@ -355,23 +433,29 @@ func (ts *HTestSuite) TestParallelInstallUserForClassicConfinement(c *C) {
355433
func (s *HTestSuite) TestExtendEnvForRunForNonClassic(c *C) {
356434
env := osutil.Environment{"TMPDIR": "/var/tmp"}
357435

358-
ExtendEnvForRun(env, mockSnapInfo, nil, nil)
436+
ExtendEnvForRun(env, mockSnapInfo, mockAppInfo, nil, nil)
359437

360438
c.Assert(env["SNAP_NAME"], Equals, "foo")
361439
c.Assert(env["SNAP_COMMON"], Equals, "/var/snap/foo/common")
362440
c.Assert(env["SNAP_DATA"], Equals, "/var/snap/foo/17")
441+
c.Assert(env["SNAP_APP_NAME"], Equals, "bar")
442+
c.Assert(env["SNAP_APP_COMMON_ID"], Equals, "io.snapcraft.foo.bar")
443+
c.Assert(env["SNAP_APP_BUS_NAME"], Equals, "io.snapcraft.foo.bar.bus")
363444

364445
c.Assert(env["TMPDIR"], Equals, "/var/tmp")
365446
}
366447

367448
func (s *HTestSuite) TestExtendEnvForRunForClassic(c *C) {
368449
env := osutil.Environment{"TMPDIR": "/var/tmp"}
369450

370-
ExtendEnvForRun(env, mockClassicSnapInfo, nil, nil)
451+
ExtendEnvForRun(env, mockClassicSnapInfo, mockAppInfoMinimal, nil, nil)
371452

372453
c.Assert(env["SNAP_NAME"], Equals, "foo")
373454
c.Assert(env["SNAP_COMMON"], Equals, "/var/snap/foo/common")
374455
c.Assert(env["SNAP_DATA"], Equals, "/var/snap/foo/17")
456+
c.Assert(env["SNAP_APP_NAME"], Equals, "bar")
457+
c.Assert(func() bool { _, ok := env["SNAP_APP_COMMON_ID"]; return ok }(), Equals, false)
458+
c.Assert(func() bool { _, ok := env["SNAP_APP_BUS_NAME"]; return ok }(), Equals, false)
375459

376460
c.Assert(env["TMPDIR"], Equals, "/var/tmp")
377461
}
@@ -392,15 +476,15 @@ func checkEnvWithComp(c *C, env osutil.Environment, compVersion string) {
392476
func (s *HTestSuite) TestExtendEnvForRunWithComponent(c *C) {
393477
env := osutil.Environment{"TMPDIR": "/var/tmp"}
394478

395-
ExtendEnvForRun(env, mockSnapInfo, mockComponentInfo, nil)
479+
ExtendEnvForRun(env, mockSnapInfo, nil, mockComponentInfo, nil)
396480
compVersion := "1.1"
397481
checkEnvWithComp(c, env, compVersion)
398482
}
399483

400484
func (s *HTestSuite) TestExtendEnvForRunWithComponentNoVersion(c *C) {
401485
env := osutil.Environment{"TMPDIR": "/var/tmp"}
402486

403-
ExtendEnvForRun(env, mockSnapInfo, mockComponentInfoNoVersion, nil)
487+
ExtendEnvForRun(env, mockSnapInfo, nil, mockComponentInfoNoVersion, nil)
404488
// Same as snap in this case
405489
compVersion := "1.0"
406490
checkEnvWithComp(c, env, compVersion)
@@ -425,7 +509,7 @@ func (s *HTestSuite) TestHiddenDirEnv(c *C) {
425509
{dir: dirs.HiddenSnapDataHomeDir, opts: &dirs.SnapDirOptions{HiddenSnapDataDir: true}},
426510
{dir: dirs.HiddenSnapDataHomeDir, opts: &dirs.SnapDirOptions{HiddenSnapDataDir: true, MigratedToExposedHome: true}}} {
427511
env := osutil.Environment{}
428-
ExtendEnvForRun(env, mockSnapInfo, nil, t.opts)
512+
ExtendEnvForRun(env, mockSnapInfo, mockAppInfo, nil, t.opts)
429513

430514
c.Check(env["SNAP_USER_COMMON"], Equals, filepath.Join(testDir, t.dir, mockSnapInfo.SuggestedName, "common"))
431515
c.Check(env["SNAP_USER_DATA"], DeepEquals, filepath.Join(testDir, t.dir, mockSnapInfo.SuggestedName, mockSnapInfo.Revision.String()))

tests/main/snap-env/task.yaml

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ prepare: |
2020
snap set system experimental.parallel-instances=true
2121
fi
2222
"$TESTSTOOLS"/snaps-state install-local-as test-snapd-tools "$NAME"
23+
"$TESTSTOOLS"/snaps-state install-local test-snapd-desktop-file-ids
2324
2425
restore: |
2526
if [[ "$SPREAD_VARIANT" == "parallel" ]]; then
@@ -57,6 +58,7 @@ execute: |
5758
MATCH "^SNAP_CONTEXT=$CTX" < snap-vars.txt
5859
# parallel-installs: $SNAP_NAME is always _the_ snap name
5960
MATCH '^SNAP_NAME=test-snapd-tools$' < snap-vars.txt
61+
MATCH '^SNAP_APP_NAME=env$' < snap-vars.txt
6062
# parallel-install: name of a particular instance
6163
MATCH "^SNAP_INSTANCE_NAME=$NAME$" < snap-vars.txt
6264
# parallel-installs: empty if none is set
@@ -67,11 +69,11 @@ execute: |
6769
# if on UC20+, then we should see an additional variable (SNAP_SAVE_DATA)
6870
if [[ "$SPREAD_SYSTEM" == ubuntu-core-2* ]]; then
6971
MATCH "^SNAP_SAVE_DATA=/var/lib/snapd/save/snap/$NAME$" < snap-vars.txt
70-
# 18 variables are expected on ubuntu-core
71-
test "$(wc -l < snap-vars.txt)" -eq 18 || { cat snap-vars.txt; exit 1; }
72+
# 19 variables are expected on ubuntu-core
73+
test "$(wc -l < snap-vars.txt)" -eq 19 || { cat snap-vars.txt; exit 1; }
7274
else
73-
# 17 variables are expected on non ubuntu-core
74-
test "$(wc -l < snap-vars.txt)" -eq 17 || { cat snap-vars.txt; exit 1; }
75+
# 18 variables are expected on non ubuntu-core
76+
test "$(wc -l < snap-vars.txt)" -eq 18 || { cat snap-vars.txt; exit 1; }
7577
fi
7678
7779
echo "Ensure that XDG environment variables are what we expect"
@@ -99,3 +101,19 @@ execute: |
99101
# parallel-installs: $HOME is set to instance specific path
100102
MATCH "^HOME=/root/snap/$NAME/x1$" < misc-vars.txt
101103
test "$(wc -l < misc-vars.txt)" -eq 4 || { cat misc-vars.txt; exit 1; }
104+
105+
echo "Collect SNAP environment variables for snap with desktop metadata"
106+
echo env | snap run --shell test-snapd-desktop-file-ids.cmd | grep -E '^SNAP_' | sort > snap-vars-desktop.txt
107+
echo "Ensure that SNAP environment variables are what we expect"
108+
MATCH '^SNAP_NAME=test-snapd-desktop-file-ids$' < snap-vars-desktop.txt
109+
MATCH '^SNAP_APP_NAME=cmd$' < snap-vars-desktop.txt
110+
MATCH '^SNAP_APP_COMMON_ID=org.example.Foo$' < snap-vars-desktop.txt
111+
MATCH '^SNAP_APP_DESKTOP_FILE=/var/lib/snapd/desktop/applications/org.example.Foo.desktop$' < snap-vars-desktop.txt
112+
# if on UC20+, then we should see an additional variable (SNAP_SAVE_DATA)
113+
if [[ "$SPREAD_SYSTEM" == ubuntu-core-2* ]]; then
114+
# 21 variables are expected on ubuntu-core
115+
test "$(wc -l < snap-vars-desktop.txt)" -eq 21 || { cat snap-vars-desktop.txt; exit 1; }
116+
else
117+
# 20 variables are expected on non ubuntu-core
118+
test "$(wc -l < snap-vars-desktop.txt)" -eq 20 || { cat snap-vars-desktop.txt; exit 1; }
119+
fi

0 commit comments

Comments
 (0)