Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/pr/181'
Browse files Browse the repository at this point in the history
* origin/pr/181: (47 commits)
  windows: fix bash script issues
  windows: rename 'artifacts' to 'files' in build plugin artifact info
  Only use admin.vm.CurrentState in WindowsQubesExecutor
  windows: allow saving generated Windows ISO under a custom name
  windows: fix review issues
  windows: ignore symlinks for copy_in
  Properly query vm state in dispvm cleanup
  windows: update README
  windows: remove qubesadmin dependency
  windows: sanitize_line() compatibility
  Refactor WindowsExecutor into WindowsQubesExecutor and SSHWindowsExecutor
  windows: fix review issues
  windows: fix shellcheck warnings in rpc files
  Fix CI config
  windows: update CI dependencies
  windows: fix mypy warnings
  windows: remove debug code
  Fix formatting
  windows: fix copy-out for no-target projects
  windows: don't copy qrexec handlers on every command
  ...
  • Loading branch information
marmarek committed Mar 8, 2025
2 parents 51fbedf + 4916714 commit ed3319c
Show file tree
Hide file tree
Showing 43 changed files with 2,952 additions and 122 deletions.
152 changes: 147 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ action on sources, like cloning and verifying Git repos, rendering a SPEC
file, generating SRPM or Debian source packages, a new cage is used. Only the
signing, publishing, and uploading processes are executed locally outside a
cage. (This will be improved in the future.) For now, only Docker, Podman,
Local, and Qubes executors are available.
Local, Qubes and Windows executors are available.


## Dependencies
Expand Down Expand Up @@ -131,6 +131,63 @@ $ qvm-prefs work-qubesos default_dispvm qubes-builder-dvm
```


## Windows executors and building Windows Tools

There are two different Windows executors: `SSHWindowsExecutor` and `WindowsQubesExecutor`.
Prerequisites for both executors are a superset of the Qubes executor (see above).
For code signing you need `osslsigncode` installed in the Linux disposable template
and the signing vault qube (see below).

### `SSHWindowsExecutor`

This executor is meant for development, it uses SSH to communicate with a Windows system
that is used for building. Scripts that can automatically create such qube are found
in the `tools/windows` directory (they require `genisoimage` installed in the disposable template
(`qubes-builder-dvm`)). You will need an unmodified Windows 10/11 installation iso and about
50GiB of disk space.

First, create an edited installation iso by running `generate-iso.sh`. This script
downloads prerequisites (OpenSSH server for Windows and Microsoft EWDK iso) and prepares
the installation image for unattended Windows installation. After the image is generated,
run `dom0/create-vm.sh` in dom0 to actually create and configure the worker qube (passing
the generated installation image as a `--iso` parameter). After the script finishes, the worker
qube is ready to use by the builder.

The worker qube has outbound network connections blocked in the firewall, this is configured
by the `create-vm.sh` script. An ssh key is generated by the `generate-iso.sh` script,
the private part is saved in `~/.ssh/win-build.key` by default while the public part
is copied to the generated Windows installation image.

If the `ssh-vm` option is set in the `builder.yml` (see below), the SSH executor automatically
starts the given vm. This option also requires the `ewdk` option, that specifies path to EWDK iso
(in `work-qubesos`). Is `ssh-vm` it's assumed that the SSH machine is configured manually.

You can also use any SSH-accessible Windows machine instead.

### `WindowsQubesExecutor`

This executor works in the same manner as the Linux Qubes executor. It requires a Windows
disposable template with Qubes Windows Tools installed (you can use the SSH executor first
to build QWT).

### General information

It is recommended to turn off Microsoft Defender in the worker qube (especially real-time
protection) because it slows down building significantly. This is not done during unattended
setup because there is no supported way for automating this. (TODO: it enables itself after
restart which is a pain for dispvms).

A separate vault-type qube is needed for code-signing Windows binaries. Let's assume it's named
`vault-sign`. This qube has access to actual signing keys used, either production ones (TODO:
in a HSM), or ephemeral self-signed keys. Communication with the `vault-sign` qube goes through
Qubes RPC: install RPC service scripts from `rpc/qubes.WinSign.*` in the vault qube (make sure
they are permanent in `/etc/qubes-rpc`, see [bind dirs](https://www.qubes-os.org/doc/bind-dirs/)).
`qubesbuilder.WinSign.Timestamp` needs to be installed in the default Linux disposable template
(`qubes-builder-dvm`). You also need to configure RPC policy in `dom0`, copy
`rpc/policy/51-qubesbuilder-windows.policy` to `/etc/qubes/policy.d` there (make sure the qube
names are correct).


## Build stages

The build process consists of the following stages:
Expand Down Expand Up @@ -160,9 +217,11 @@ Currently, only these are used:
- `source` --- Manages general distribution sources
- `source_rpm` --- Manages RPM distribution sources
- `source_deb` --- Manages Debian distribution sources
- `source_windows` --- Manages Windows sources
- `build` --- Manages general distribution building
- `build_rpm` --- Manages RPM distribution building
- `build_deb` --- Manages Debian distribution building
- `build_windows` --- Manages Windows building (Visual Studio solutions)
- `sign` --- Manages general distribution signing
- `sign_rpm` --- Manages RPM distribution signing
- `sign_deb` --- Manages Debian distribution signing
Expand Down Expand Up @@ -390,9 +449,10 @@ We provide the following list of available keys:
- `vm` --- `vm` package set content.
- `rpm` --- RPM plugins content.
- `deb` --- Debian plugins content.
- `source` --- Fetch and source plugins (`fetch`, `source`, `source_rpm`, and
`source_deb`) content.
- `build` --- Build plugins content (`build`, `build_rpm`, and `build_deb`).
- `windows` --- Windows plugin content.
- `source` --- Fetch and source plugins (`fetch`, `source`, `source_rpm`,
`source_deb` and `source_windows`) content.
- `build` --- Build plugins content (`build`, `build_rpm`, `build_deb` and `build_windows`).
- `create-archive` --- Create source component directory archive (default:
`True` unless `files` is provided and not empty).
- `commands` --- Execute commands before plugin or distribution tools
Expand Down Expand Up @@ -429,6 +489,17 @@ Here is a non-exhaustive list of distribution-specific keys:
- `host-fc32` --- Fedora 32 for the `host` package set content only
- `vm-bullseye` --- Bullseye for the `vm` package set only
`build_windows` specific: all output artifacts for a component need to be specified in
`.qubesbuilder` as lists of paths (relative to component root) with the following keys:
- `bin` --- binaries (`.exe`, `.dll`, `.sys` and all files that can be PE-signed)
- `inc` --- devel header files that are dependencies for other components
- `lib` --- linker libraries that are dependencies for other components
`skip-test-sign` option can be specified to provide a list of binaries that should not be
signed with a test key (only used for the final installer binary currently, since
the self-signed certificate is in the installer itself so the binary can't be verified
if test-signed).
Inside each top level, it defines what plugin entry points like `rpm`, `deb`,
and `source` will take as input. Having both `PACKAGE_SET` and
`PACKAGE_SET-DISTRIBUTION_NAME` with common keys means that it is up to the
Expand All @@ -449,6 +520,8 @@ currently-supported placeholders:
- `@DISTFILES_DIR@` --- Replaced by `/builder/distfiles` (inside a cage)
- `@SOURCE_DIR@` --- Replaced by `/builder/<COMPONENT_NAME>` (inside a cage
where, `<COMPONENT_NAME>` is the component directory name)
- `@CONFIGURATION@` --- `build_windows` specific, replaced by the project configuration
(`Debug` / `Release`)
### Examples
Expand Down Expand Up @@ -675,6 +748,32 @@ distribution tools like `dpkg-*`. This is only available for Debian
distributions and not RPM distributions, as similar processing is currently not
needed.
Here is an example for a Windows component (`core-vchan-xen`):
```yaml
host:
rpm:
build:
- rpm_spec/libvchan.spec
vm:
rpm:
build:
- rpm_spec/libvchan.spec
(...)
windows:
build:
- windows/vs2022/core-vchan-xen.sln
bin:
- windows/vs2022/x64/@CONFIGURATION@/libvchan/libvchan.dll
inc:
- windows/include/libvchan.h
lib:
- windows/vs2022/x64/@CONFIGURATION@/libvchan/libvchan.lib
```
The `build` stage specifies a Visual Studio solution to be built. `bin`, `inc` and `lib` keys
specify output artifacts.
## Qubes builder configuration
Options available in `builder.yml`:
Expand Down Expand Up @@ -742,14 +841,27 @@ Options available in `builder.yml`:
- `templates: str` --- Testing repository for templates at publish stage. This is either `templates-itl-testing` or `templates-community-testing`.
- `executor: Dict` --- Specify default executor to use.
- `type: str` --- Executor type: qubes, docker, podman or local.
- `type: str` --- Executor type: qubes, docker, podman, local or windows.
- `options: Dict`:
- `image: str` --- Container image to use. Specific to docker or podman type.
- `dispvm: str` --- Disposable template VM to use (use `"@dispvm"` to use the calling qube `default_dispvm` property or specify a name).
- `directory: str` --- Base directory for local executor to create temporary directories.
- `clean: bool` --- Clean container, disposable qube or temporary local folder (default `true`).
- `clean-on-error: bool` --- Clean container, disposable qube or temporary local folder if any error occurred. Default is value set by `clean`.
- Options specific to the `windows` and `windows-ssh` executors (see `example-configs/windows-tools.yml`):
- `user: str` --- Name of the user account in the worker Windows machine/VM (default: `user`).
- `threads: int` --- Number of parallel threads to use for MSBuild (default: 1).
- `ewdk: str` --- Path to the EWDK iso file that will be attached to the worker qube.
- Options specific to the `windows` executor:
- `dispvm: str` --- Name of the disposable Windows template (default: `win-build`).
- Options specific to the `windows-ssh` executor:
- `ssh-key-path: str` --- Path to the private ssh key used for communication with the worker machine (default: `~/.ssh/win-build.key`).
- `ssh-ip: str` --- IP address to use when connecting to the worker machine.
- `ssh-vm: str` --- Name of the worker qube (optional). If specified, this qube is started automatically and the EWDK iso is attached to it as a block device.
- `stages: List[str, Dict]` --- List of stages to trigger.
- `<stage_name>: str` --- Stage name.
- `<stage_name>: Dict` --- Stage name provided as dict to override executor to use.
Expand Down Expand Up @@ -859,3 +971,33 @@ For the `fetch` stage, the Qubes executor with disposable template `qubes-builde
For the `build` stage of `vm-fc42`, the Podman executor with container image `fedoraimg` will be used.
For the `sign` stage, the Qubes executor with disposable template `signing-access-dvm` will be used for both `vm-fc42` and `vm-jammy`
For the `prep` stage of `vm-jammy`, the Local executor with base directory `/some/path` will be used.
### Windows-specific build stage options
Options related to Qubes Windows Tools can be specified under the `build` stage of a Windows distribution, like this:
```yaml
distributions:
- vm-win10:
stages:
- build:
configuration: release
sign-qube: vault-sign
sign-key-name: "Qubes Windows Tools"
test-sign: true
executor:
type: windows # or windows-ssh
options:
user: user
threads: 1
dispvm: win-build
ewdk: "dev:loop1"
#ssh-key-path: /home/user/.ssh/win-build.key
#ssh-ip: 10.137.0.20
```
- `configuration: str` --- build configuration (`debug` / `release`) (default: `release`).
- `sign-qube: str` --- name of the vault qube performing code signing, see the Windows executor description above.
- `sign-key-name: str` --- name of the signing key to use. For test keys this becomes the subject of the self-signed certificate.
- `test-sign: bool` --- code signing type, `true` (default) or `false`. Test signing generates ephemeral self-signed
keys for each component. Production signing uses an already existing key signed by a public CA (TODO: HSM).
59 changes: 59 additions & 0 deletions example-configs/windows-tools.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
git:
prefix: omeg/qubes-
branch: omeg/builder-v2
maintainers:
# omeg
- 'CE8060B48282B234AE0A7815D32BF219E67BA830'

#increment-devel-versions: true

debug: true
verbose: true

skip-git-fetch: false

# this is for anything other than building, so source fetching etc
executor:
type: qubes
options:
dispvm: qubes-builder-dvm

# dev only
less-secure-signed-commits-sufficient:
- vmm-xen-windows-pvdrivers
- core-vchan-xen
- windows-utils
- core-qubesdb
- core-agent-windows
- gui-common
- gui-agent-windows
- installer-windows-tools

distributions:
- vm-win10:
stages:
- build:
configuration: release
sign-qube: vault-sign
sign-key-name: "Qubes Windows Tools"
test-sign: true
executor:
#type: windows-ssh
type: windows
options:
dispvm: win-build
user: user
ewdk: tools/windows/ewdk.iso
threads: 1
#ssh-ip: 10.137.0.20
#ssh-key-path: /home/user/.ssh/win-build.key

components:
- vmm-xen-windows-pvdrivers
- core-vchan-xen
- windows-utils
- core-qubesdb
- core-agent-windows
- gui-common
- gui-agent-windows
- installer-windows-tools
5 changes: 4 additions & 1 deletion qubesbuilder/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,10 @@ def sanitize_line(untrusted_line: bytes):
if 0x20 <= c <= 0x7E:
pass
else:
line[i] = 0x2E
if c == 0x0D: # windows newline
line[i] = 0x20
else:
line[i] = 0x2E
return bytearray(line).decode("ascii")


Expand Down
10 changes: 9 additions & 1 deletion qubesbuilder/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@
from qubesbuilder.executors import ExecutorError
from qubesbuilder.executors.container import ContainerExecutor
from qubesbuilder.executors.local import LocalExecutor
from qubesbuilder.executors.qubes import LinuxQubesExecutor
from qubesbuilder.executors.qubes import (
LinuxQubesExecutor,
WindowsQubesExecutor,
)
from qubesbuilder.executors.windows import SSHWindowsExecutor
from qubesbuilder.pluginmanager import PluginManager
from qubesbuilder.plugins import (
DistributionPlugin,
Expand Down Expand Up @@ -572,6 +576,10 @@ def get_executor(options):
executor = LocalExecutor(**executor_options) # type: ignore
elif executor_type == "qubes":
executor = LinuxQubesExecutor(**executor_options) # type: ignore
elif executor_type == "windows":
executor = WindowsQubesExecutor(**executor_options) # type: ignore
elif executor_type == "windows-ssh":
executor = SSHWindowsExecutor(**executor_options) # type: ignore
else:
raise ExecutorError("Cannot determine which executor to use.")
return executor
Expand Down
15 changes: 15 additions & 0 deletions qubesbuilder/distribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@

DEBIAN_ARCHITECTURE = {"x86_64": "amd64", "ppc64le": "ppc64el"}

WINDOWS = {
"win10": ("windows", "10"),
"win11": ("windows", "11"),
}


class QubesDistribution:
def __init__(self, distribution: str, **kwargs):
Expand All @@ -65,6 +70,7 @@ def __init__(self, distribution: str, **kwargs):
is_ubuntu = UBUNTU.get(self.name, None)
is_archlinux = self.name == "archlinux"
is_gentoo = self.name == "gentoo"
is_windows = WINDOWS.get(self.name, None)
if is_fedora:
self.fullname = "fedora"
self.version = is_fedora.group(1)
Expand Down Expand Up @@ -101,6 +107,10 @@ def __init__(self, distribution: str, **kwargs):
self.version = "rolling"
self.tag = "gentoo"
self.type = "gentoo"
elif is_windows:
self.fullname, self.version = WINDOWS[self.name]
self.tag = "windows"
self.type = "windows"
else:
raise DistributionError(
f"Unsupported distribution '{self.distribution}'."
Expand Down Expand Up @@ -140,3 +150,8 @@ def is_archlinux(self) -> bool:

def is_gentoo(self) -> bool:
return self.name == "gentoo"

def is_windows(self) -> bool:
if WINDOWS.get(self.name, None):
return True
return False
Loading

0 comments on commit ed3319c

Please sign in to comment.