From 301682c9288228e809e6bd73b73bd30cf4d878ae Mon Sep 17 00:00:00 2001 From: Artem Konev Date: Fri, 19 Jun 2026 14:00:02 +0100 Subject: [PATCH] Doc: Add how-to for designing interface layout --- .../design-interface-layout.rst | 221 ++++++++++++++++++ docs/how-to/customize-workshops/index.rst | 1 + .../.workshop/consumer/sdk.yaml | 14 ++ .../.workshop/graft.yaml | 13 ++ .../.workshop/plainconsumer/sdk.yaml | 8 + .../.workshop/provider-a/hooks/setup-base | 4 + .../.workshop/provider-a/sdk.yaml | 13 ++ .../.workshop/provider-b/hooks/setup-base | 4 + .../.workshop/provider-b/sdk.yaml | 13 ++ .../dev.connected.yaml | 10 + .../dev.reordered.yaml | 10 + .../design-interface-layout/dev.yaml | 6 + .../design-interface-layout/task.yaml | 37 +++ 13 files changed, 354 insertions(+) create mode 100644 docs/how-to/customize-workshops/design-interface-layout.rst create mode 100644 tests/docs-how-to/design-interface-layout/.workshop/consumer/sdk.yaml create mode 100644 tests/docs-how-to/design-interface-layout/.workshop/graft.yaml create mode 100644 tests/docs-how-to/design-interface-layout/.workshop/plainconsumer/sdk.yaml create mode 100755 tests/docs-how-to/design-interface-layout/.workshop/provider-a/hooks/setup-base create mode 100644 tests/docs-how-to/design-interface-layout/.workshop/provider-a/sdk.yaml create mode 100755 tests/docs-how-to/design-interface-layout/.workshop/provider-b/hooks/setup-base create mode 100644 tests/docs-how-to/design-interface-layout/.workshop/provider-b/sdk.yaml create mode 100644 tests/docs-how-to/design-interface-layout/dev.connected.yaml create mode 100644 tests/docs-how-to/design-interface-layout/dev.reordered.yaml create mode 100644 tests/docs-how-to/design-interface-layout/dev.yaml create mode 100644 tests/docs-how-to/design-interface-layout/task.yaml diff --git a/docs/how-to/customize-workshops/design-interface-layout.rst b/docs/how-to/customize-workshops/design-interface-layout.rst new file mode 100644 index 000000000..bf6d90ffb --- /dev/null +++ b/docs/how-to/customize-workshops/design-interface-layout.rst @@ -0,0 +1,221 @@ +.. _how_design_interface_layout: + +.. meta:: + :description: Shape a workshop's interface topology with explicit connection + entries in the workshop definition: survey what auto-connects, + wire a consumer to a specific provider, graft a missing plug, + or rewire a running workshop with workshop connect. + +How to design the interface layout of a workshop +================================================ + +.. @tests in tests/docs-how-to/design-interface-layout/task.yaml + +.. @artefact interface connection +.. @artefact workshop definition + +You can shape the topology of a workshop +by writing explicit plug-to-slot connections in the workshop definition. +Use explicit connections +when several SDKs in the workshop expose or consume the same interface +and you want to be specific about which provider satisfies which consumer, +when auto-connection lands a plug on a slot you did not intend, +or when a consumer SDK ships no plug for a capability you want it to use. +You need a workshop definition under :file:`.workshop/` +that lists at least two SDKs, one with a slot and one with a matching plug. + +This is a different problem from same-interface plug conflicts, +where two plugs would compete over the same target. +For that case, see :ref:`how_resolve_plug_conflicts`, +which uses an inline :samp:`bind:` attribute to delegate one plug to another. + + +Survey the plugs and slots in scope +----------------------------------- + +Launch the workshop once to see what |ws_markup| connects on its own. +The examples here use three in-project SDKs +that live under :file:`.workshop/` next to the definition: +:samp:`provider-a` and :samp:`provider-b` +each expose a mount slot named :samp:`data`, +and :samp:`consumer` declares a mount plug named :samp:`feed`. + +.. code-block:: console + + $ workshop launch dev + $ workshop connections dev + + INTERFACE PLUG SLOT NOTES + mount - dev/provider-a:data - + mount - dev/provider-b:data - + mount dev/consumer:feed dev/system:mount - + + +The output lists every plug and slot in the workshop, +the slot each plug is connected to (if any), +and any notes on the connection. +:samp:`consumer:feed` landed on the system SDK's default mount slot, +not on either regular provider, +because mount plugs auto-connect to system SDK slots by default. +The :samp:`provider-a:data` and :samp:`provider-b:data` slots +stay listed but unconnected: +regular SDK mount slots are not reached by auto-connection by default, +even when a matching plug is in scope. +The result is a working workshop, but probably not the one you intended. +A regular SDK slot is wired +either by a top-level :samp:`connections:` entry in the definition +or manually with :command:`workshop connect`. + + +Wire a consumer to a specific provider +-------------------------------------- + +Add a top-level :samp:`connections:` list to the workshop definition, +pairing the plug with the slot you want it to use: + +.. code-block:: yaml + :caption: .workshop/dev.yaml + :emphasize-lines: 8-10 + + name: dev + base: ubuntu@22.04 + sdks: + - name: project-provider-a + - name: project-provider-b + - name: project-consumer + + connections: + - plug: consumer:feed + slot: provider-b:data + + +Each entry uses the :samp:`:` form on both sides. +In-project SDKs take the :samp:`project-` prefix +in the :samp:`sdks:` list only; +:samp:`connections:` entries and CLI output use the bare name. +After :command:`workshop refresh`, +|ws_markup| applies the listed pairing +regardless of what other slots could have matched it: + +.. code-block:: console + + $ workshop refresh dev + $ workshop connections dev + + INTERFACE PLUG SLOT NOTES + mount - dev/provider-a:data - + mount dev/consumer:feed dev/provider-b:data - + + +This decision is persistent; +re-launching the workshop or recreating it +applies the same pairing every time. +To inspect the resolved mount details, run :command:`workshop info dev`, +which lists each connected mount plug +along with the source path the slot exposed +and the target path inside the workshop. + + +Graft a missing plug onto a consumer SDK +---------------------------------------- + +Suppose a consumer SDK ships no plug for the capability you want it to use. +The workshop definition can add one: +declare the plug under the SDK's entry in :samp:`sdks`, +then connect it as before. +This example pairs :samp:`provider-sdk`, +which exposes a mount slot named :samp:`bin`, +with :samp:`consumer-sdk`, which ships no matching plug: + +.. code-block:: yaml + :caption: .workshop/dev.yaml + :emphasize-lines: 5-9, 11-13 + + name: dev + base: ubuntu@22.04 + sdks: + - name: project-provider-sdk + - name: project-consumer-sdk + plugs: + tools: + interface: mount + workshop-target: /home/workshop/.local/share/tools + + connections: + - plug: consumer-sdk:tools + slot: provider-sdk:bin + + +This grafts a new plug onto :samp:`consumer-sdk` +without modifying the SDK itself. +The publisher does not need to ship every plug +their users might want; +the workshop author can add the missing piece locally. + + +Rewire a running workshop with workshop connect +----------------------------------------------- + +For a one-off change, :command:`workshop connect` rewires a running workshop +without editing the workshop definition. +Pass the plug and the target slot explicitly: + +.. code-block:: console + + $ workshop disconnect dev/consumer:feed + $ workshop connect dev/consumer:feed dev/provider-a:data + $ workshop connections dev + + INTERFACE PLUG SLOT NOTES + mount - dev/provider-b:data - + mount dev/consumer:feed dev/provider-a:data manual + + +The :samp:`manual` note in the :samp:`NOTES` column flags that the connection +came from a CLI invocation rather than the workshop definition +or the auto-connection mechanism. + +The workshop definition on disk is unchanged, +and the runtime marks are not reconciled with it: +the next :command:`workshop refresh` that applies updates +drops connections made with :command:`workshop connect`, +while plugs disconnected with :command:`workshop disconnect` +stay disconnected, +unless the disconnection was made with :option:`!--forget`. +In the example above, +a refresh thus leaves :samp:`consumer:feed` unconnected: +the manual connection to :samp:`provider-a:data` is dropped, +and the definition's pairing with :samp:`provider-b:data` is not restored +because the plug was manually disconnected from it. +Running :command:`workshop remove` discards all runtime marks, +so a subsequent :command:`workshop launch` starts from the definition. + +For a topology that survives refreshes +and travels with the project, +edit the workshop definition instead. + + +See also +-------- + +Explanation: + +- :ref:`exp_in_project_sdk` +- :ref:`exp_interface_concepts` +- :ref:`exp_plug_bindings` +- :ref:`exp_plugs_slots` + + +How-to guides: + +- :ref:`how_resolve_plug_conflicts` + + +Reference: + +- :ref:`ref_workshop_connect` +- :ref:`ref_workshop_connections` +- :ref:`ref_workshop_definition` +- :ref:`ref_workshop_disconnect` +- :ref:`ref_workshop_info` +- :ref:`ref_workshop_refresh` diff --git a/docs/how-to/customize-workshops/index.rst b/docs/how-to/customize-workshops/index.rst index 6ef4f43d0..75c0468da 100644 --- a/docs/how-to/customize-workshops/index.rst +++ b/docs/how-to/customize-workshops/index.rst @@ -15,6 +15,7 @@ workshops in parallel: Add actions to workshops Add mounts + Design the interface layout Forward ports Move projects around Use multiple workshops diff --git a/tests/docs-how-to/design-interface-layout/.workshop/consumer/sdk.yaml b/tests/docs-how-to/design-interface-layout/.workshop/consumer/sdk.yaml new file mode 100644 index 000000000..2c1dcd5bf --- /dev/null +++ b/tests/docs-how-to/design-interface-layout/.workshop/consumer/sdk.yaml @@ -0,0 +1,14 @@ +name: consumer +base: ubuntu@22.04 +summary: Synthesized consumer for the ambiguity test +description: | + Declares a mount plug named "feed". With provider-a and provider-b both + present, autowiring is ambiguous and the workshop definition must specify + which slot to use. + +sdkcraft-started-at: "2026-05-14T00:00:00.000000+00:00" + +plugs: + feed: + interface: mount + workshop-target: /home/workshop/feed diff --git a/tests/docs-how-to/design-interface-layout/.workshop/graft.yaml b/tests/docs-how-to/design-interface-layout/.workshop/graft.yaml new file mode 100644 index 000000000..e326987f8 --- /dev/null +++ b/tests/docs-how-to/design-interface-layout/.workshop/graft.yaml @@ -0,0 +1,13 @@ +name: graft +base: ubuntu@22.04 +sdks: + - name: project-provider-a + - name: project-plainconsumer + plugs: + feed: + interface: mount + workshop-target: /home/workshop/feed + +connections: + - plug: plainconsumer:feed + slot: provider-a:data diff --git a/tests/docs-how-to/design-interface-layout/.workshop/plainconsumer/sdk.yaml b/tests/docs-how-to/design-interface-layout/.workshop/plainconsumer/sdk.yaml new file mode 100644 index 000000000..b60279f42 --- /dev/null +++ b/tests/docs-how-to/design-interface-layout/.workshop/plainconsumer/sdk.yaml @@ -0,0 +1,8 @@ +name: plainconsumer +base: ubuntu@22.04 +summary: Synthesized consumer that ships no plug, for the graft scenario +description: | + Ships no mount plug. The graft workshop declares a "feed" plug inline under + this SDK's sdks entry to demonstrate grafting a plug the SDK did not ship. + +sdkcraft-started-at: "2026-05-14T00:00:00.000000+00:00" diff --git a/tests/docs-how-to/design-interface-layout/.workshop/provider-a/hooks/setup-base b/tests/docs-how-to/design-interface-layout/.workshop/provider-a/hooks/setup-base new file mode 100755 index 000000000..ace84d5a5 --- /dev/null +++ b/tests/docs-how-to/design-interface-layout/.workshop/provider-a/hooks/setup-base @@ -0,0 +1,4 @@ +#!/bin/bash +install -d -m 0755 -o 1000 -g 1000 /home/workshop/provider-a-data +echo "from-a" >/home/workshop/provider-a-data/marker +chown 1000:1000 /home/workshop/provider-a-data/marker diff --git a/tests/docs-how-to/design-interface-layout/.workshop/provider-a/sdk.yaml b/tests/docs-how-to/design-interface-layout/.workshop/provider-a/sdk.yaml new file mode 100644 index 000000000..df7e24b98 --- /dev/null +++ b/tests/docs-how-to/design-interface-layout/.workshop/provider-a/sdk.yaml @@ -0,0 +1,13 @@ +name: provider-a +base: ubuntu@22.04 +summary: First synthesized provider for the ambiguity test +description: | + Exposes a mount slot named "data". Combined with provider-b in the same + workshop, this creates ambiguous autowiring for the consumer's "feed" plug. + +sdkcraft-started-at: "2026-05-14T00:00:00.000000+00:00" + +slots: + data: + interface: mount + workshop-source: /home/workshop/provider-a-data diff --git a/tests/docs-how-to/design-interface-layout/.workshop/provider-b/hooks/setup-base b/tests/docs-how-to/design-interface-layout/.workshop/provider-b/hooks/setup-base new file mode 100755 index 000000000..8726619a4 --- /dev/null +++ b/tests/docs-how-to/design-interface-layout/.workshop/provider-b/hooks/setup-base @@ -0,0 +1,4 @@ +#!/bin/bash +install -d -m 0755 -o 1000 -g 1000 /home/workshop/provider-b-data +echo "from-b" >/home/workshop/provider-b-data/marker +chown 1000:1000 /home/workshop/provider-b-data/marker diff --git a/tests/docs-how-to/design-interface-layout/.workshop/provider-b/sdk.yaml b/tests/docs-how-to/design-interface-layout/.workshop/provider-b/sdk.yaml new file mode 100644 index 000000000..34ce49271 --- /dev/null +++ b/tests/docs-how-to/design-interface-layout/.workshop/provider-b/sdk.yaml @@ -0,0 +1,13 @@ +name: provider-b +base: ubuntu@22.04 +summary: Second synthesized provider for the ambiguity test +description: | + Exposes a mount slot named "data" with the same interface as provider-a, + creating the ambiguous autowiring scenario the how-to teaches you to resolve. + +sdkcraft-started-at: "2026-05-14T00:00:00.000000+00:00" + +slots: + data: + interface: mount + workshop-source: /home/workshop/provider-b-data diff --git a/tests/docs-how-to/design-interface-layout/dev.connected.yaml b/tests/docs-how-to/design-interface-layout/dev.connected.yaml new file mode 100644 index 000000000..29f533db8 --- /dev/null +++ b/tests/docs-how-to/design-interface-layout/dev.connected.yaml @@ -0,0 +1,10 @@ +name: dev +base: ubuntu@22.04 +sdks: + - name: project-provider-a + - name: project-provider-b + - name: project-consumer + +connections: + - plug: consumer:feed + slot: provider-b:data diff --git a/tests/docs-how-to/design-interface-layout/dev.reordered.yaml b/tests/docs-how-to/design-interface-layout/dev.reordered.yaml new file mode 100644 index 000000000..cf484ab95 --- /dev/null +++ b/tests/docs-how-to/design-interface-layout/dev.reordered.yaml @@ -0,0 +1,10 @@ +name: dev +base: ubuntu@22.04 +sdks: + - name: project-provider-b + - name: project-provider-a + - name: project-consumer + +connections: + - plug: consumer:feed + slot: provider-b:data diff --git a/tests/docs-how-to/design-interface-layout/dev.yaml b/tests/docs-how-to/design-interface-layout/dev.yaml new file mode 100644 index 000000000..f2ee27c70 --- /dev/null +++ b/tests/docs-how-to/design-interface-layout/dev.yaml @@ -0,0 +1,6 @@ +name: dev +base: ubuntu@22.04 +sdks: + - name: project-provider-a + - name: project-provider-b + - name: project-consumer diff --git a/tests/docs-how-to/design-interface-layout/task.yaml b/tests/docs-how-to/design-interface-layout/task.yaml new file mode 100644 index 000000000..d4fe3c26d --- /dev/null +++ b/tests/docs-how-to/design-interface-layout/task.yaml @@ -0,0 +1,37 @@ +summary: Test 'How to design the interface layout of a workshop' to ensure it's operational +restore: | + . "$TESTSLIB"/utils.sh + workshop_exec remove dev graft || true +execute: | + . "$TESTSLIB"/utils.sh + chown -R ubuntu:ubuntu . + + echo "Stage 1: launch without connections; mount plugs do not auto-connect to regular SDK slots, so consumer:feed falls back to system:mount." + cp dev.yaml .workshop/dev.yaml + workshop_exec launch dev + workshop_exec connections dev | MATCH '^mount.+dev/consumer:feed.+dev/system:mount' + + echo "Stage 2: add a connections entry pointing the plug at provider-b; refresh; verify the chosen slot connects and the marker is from-b." + cp dev.connected.yaml .workshop/dev.yaml + workshop_exec refresh dev + workshop_exec connections dev | MATCH '^mount.+dev/consumer:feed.+dev/provider-b:data' + workshop_exec exec dev -- cat /home/workshop/feed/marker | MATCH '^from-b$' + + echo "Stage 3: disconnect then connect to provider-a at runtime; the runtime change is visible (manual) but the on-disk definition is unchanged." + EXPECTED_YAML_HASH=$(sha256sum .workshop/dev.yaml | cut -d' ' -f1) + workshop_exec disconnect dev/consumer:feed + workshop_exec connect dev/consumer:feed dev/provider-a:data + workshop_exec connections dev | MATCH '^mount.+dev/consumer:feed.+dev/provider-a:data.+manual' + workshop_exec exec dev -- cat /home/workshop/feed/marker | MATCH '^from-a$' + ACTUAL_YAML_HASH=$(sha256sum .workshop/dev.yaml | cut -d' ' -f1) + test "$EXPECTED_YAML_HASH" = "$ACTUAL_YAML_HASH" + + echo "Stage 4: a refresh that applies updates drops the manual connection and keeps the manually disconnected pairing disconnected, so the plug ends up unconnected." + cp dev.reordered.yaml .workshop/dev.yaml + workshop_exec refresh dev + workshop_exec connections dev | MATCH '^mount +dev/consumer:feed +- +-' + + echo "Stage 5: graft a plug onto an SDK that ships none; launch the graft workshop; the grafted plug connects to provider-a and reads from-a." + workshop_exec launch graft + workshop_exec connections graft | MATCH '^mount.+graft/plainconsumer:feed.+graft/provider-a:data' + workshop_exec exec graft -- cat /home/workshop/feed/marker | MATCH '^from-a$'