| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
| |
nix-build tests -A only --argstr only REGEX runs the scenarios whose
names match the regular expression REGEX in a single VM test, like the
smoke test, e.g. --argstr only 'lock-hooks-.*' for lock-hooks on all
three kinds of targets.
|
| |
|
|
|
|
|
| |
git sources refused work trees with hooks, but populate's Git doesn't
run hooks, so git-foreign's work tree with a hook is fine to use, as it
expects. Only Nix's Git runs them, in a git+file: flake, so drawing
inputs refers to a work tree with hooks as path: instead.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Writing the lock file of a git+file: flake, Nix runs Git, with the
repository's configuration and hooks, which may run commands. A .git
in a source's directory can come from anywhere, e.g. one planted in a
materialized flake, or one a file source keeps. So drawing inputs
refers to a flake as git+file: only if git_problems finds nothing wrong
with its .git directory, the way git sources check their work trees,
and else as path:.
lock-foreign-git plants a .git in a materialized flake and in a file
source that keeps it, whose configuration names an fsmonitor command.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
git submodule update uses the .git of a submodule as it finds it in the
work tree: a repository of its own, with configuration that runs
commands, or a file naming a repository elsewhere. So git_problems
also requires each .git below the work tree to be a file naming a
repository in .git/modules. And it refuses hooks in .git/hooks, which
populate's Git doesn't run, but Nix's does, writing the lock file of a
flake in a git source. It's a shell function now, taking the work tree
at run time, so drawing inputs can use it too.
git-submodule plants a submodule's .git in the work tree: a repository
of its own whose configuration names a filter, and a file naming a
repository elsewhere. The fixture withsub has gitsrc as its submodule
sub.
|
| |
|
|
|
|
|
|
|
| |
With allocateTTY, nixos-rebuild runs with a terminal, so it shows colours
and progress: on remote targets, ssh gets -t, which allocates a terminal
if its own input is one, and on the local target, expect's unbuffer
provides one.
The nixos-rebuild stand-in records whether its output is a terminal.
|
| |
|
|
|
|
|
|
| |
A ref that consists of 4 to 40 hex digits is a commit, which populate
fetches only if the work tree lacks it, unless fetchAlways is set. Any
other ref is a branch or tag, which populate fetches on every run,
resetting the work tree to the commit fetched. Resetting to the name
itself resolves to a local branch, which fetching doesn't move.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Nix ignores an input that --override-input names but the flake doesn't
have, with a warning, so a misspelt input name in inputs draws nothing,
and the source keeps using its own input.
Before changing anything, populate locks the flake of each source that
draws inputs, with an empty flake in place of each input drawn, which
may be unreachable, and fails, naming the source and the input, unless
the flake has each input drawn. Below an input drawn from a source, the
path continues in the flake of that source.
An edge case misspells an input of the source's flake, and one below an
input drawn from a source, both ways of drawing inputs.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Populate writes some files in place, and makes files writable, which
reaches beyond the target directory where such a file is a symlink or a
hard link to one elsewhere:
- The key of a derived copy gets written to a new file, which then
replaces it, instead of written through a symlink at its place.
- Lock files get copied to a new file before Nix rewrites them, instead
of to flake.lock.populate, which a source may contain as a symlink.
- Before rsync syncs a source, each file with other hard links gets
replaced by a copy of its own. rsync fixes the mode and time of a file
it takes for unchanged in place, which changes every hard link to it:
one elsewhere, e.g. in /etc/nixos, or a backup's.
- Removing state makes only directories writable, which is what
removing what's in them needs, instead of everything, including files
that are hard links to ones elsewhere.
- The sentinel file only gets created if it's missing, instead of
touched on every run.
- A link only counts as pointing into the store if its resolved path is
in the store, so a link like /nix/store/../.. gets replaced.
Safety scenarios cover a symlink in place of a key, symlinks in a source
named like the lock file's copies, a hard link to a read-only file
outside in the target directory, and in stale state.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A .git directory names places elsewhere in more ways than being a
symlink or a file: core.worktree, symlinks to a configuration, to
objects, or to refs elsewhere, a commondir file naming another
repository, and linked work trees. Git's commands follow them, so
fetching, resetting, setting the remote's URL, and cleaning change what
is elsewhere: with core.worktree, git clean removes everything in the
work tree it names. And Git runs commands the configuration names, like
core.fsmonitor, and hooks.
Populate only uses a work tree whose .git directory has no symlinks, no
commondir file, no linked work trees, and only configuration that git
clone and populate write, where submodules' repositories may have their
work tree in the work tree. Otherwise populating fails with an error
telling what's wrong, unless the new option reclone replaces the work
tree by a fresh clone. Git runs without hooks and without fsmonitor in
any case.
The work tree also gets reset to the commit if HEAD is there already: a
lock file rewritten in place, or created and staged by Nix, stays
otherwise once the source stops drawing inputs.
A safety scenario refuses work trees with core.worktree, a symlinked
configuration, a commondir file, and core.fsmonitor, replaces them with
reclone, and checks that a hook doesn't run. A git scenario stops
drawing inputs and checks that the work tree is clean.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The matrices cross every option of a source type with every other one,
also where the code handling one never looks at the other. Scenarios
for such combinations are exhaustive, and run in their own tests, which
nix-build tests doesn't build, but nix-build tests -A exhaustive does:
- file and git: drawing inputs combined with rsync's or Git's options,
since drawing inputs happens after delivery.
- flake: materialize combined with how the flake gets there, except for
flakes in Git work trees and in subdirectories, since materializing
copies what got delivered.
- flake-input: materialize for inputs other than dep.
- deploy: /etc/nixos/flake.nix combined with deploying a flake, since it
only matters without one.
- edge-cases: generated flakes where only delivery matters, and remote
targets where drawing inputs alone matters, which happens on the target
alike for every kind of target.
That's 181 of 707 scenarios.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Several checks would pass with the feature they cover broken, or cover
it only by chance:
- Phases can state what a succeeding populate prints. That covers the
warnings of insecureLinks, the lines pruning prints, the one about a
replaced symlink, and what rsync logs about each item.
- The insecureLinks scenarios take their expectations from the version of
rsync, so with an rsync older than 3.5.0 they pass without the check
ever running. New tests, rsync-3-4-1 and rsync-3-5-0, run them with
these versions, which tests/rsync.nix builds from their release
tarballs, with ACLs and extended attributes, which backups use.
- Pruning has a leftover upload to remove, and a source that stops
having inputs to root leaves no empty directory of roots.
- A full clone has every commit from the start, so the fetch for a commit
it lacks never runs. A git scenario deploys a commit that appears on
the server after cloning.
- The jq test also runs the Nix function that finds a flake's
subdirectory on the target, over both shapes of nix flake metadata's
output.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Names and paths with characters that mean something to shells, to find,
or in URLs get handled as what they are, or refused:
- Pruning, and removing state that sources don't need anymore, list
entries with find instead of a loop reading names, which strips blanks
at the ends of names and splits names at newlines. So an entry named
"trail " gets pruned instead of reported as pruned and left.
- Pruning doesn't make entries writable before removing them, which
would change the mode of files elsewhere that are hard links to files
therein.
- Source names are portable file names: letters, digits, and ._-, not
starting with -. nixpkgs' writers accept nothing else in the names of
the scripts populating the sources, which contain the source names, and
fail with an assertion that doesn't mention the source.
- Paths get percent-encoded where they go into URLs for Nix, which
refuses blanks there and takes ? and # for delimiters.
- rsync gets the target path protected from the target's shell
(--protect-args), which expands wildcards in it otherwise: a file source
delivered to /var/src/a?c ends up in /var/src/abc if that exists.
A safety scenario deploys every kind of state to a target path with a
blank, wildcards, #, and %. An edge case prunes entries with blanks at
the ends of their names, with a newline, and with wildcards, and the
scenario pruning links also prunes hard links to a read-only file
outside, and lists what's left in the C locale, where ls sorts .populate
first. The scenario with unusual source names also draws inputs from a
source named self.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
rsync reads .rsync-filter files in the directories above a source as
well, and their rules apply to the transfer. The check for files that
rsync 3.5.0 and later can't read only starts if the source itself
contains one, so a subdirectory of a repository with a filter file at
its top gets synced without it, silently. The check also starts for
filter files above the source.
The check's dry run is the transfer it stands for, as far as filtering
goes:
- It gets the source's exclude and filters, so it doesn't enter
directories the transfer skips.
- It doesn't run for transfers that apply no .rsync-filter files, i.e.
uploads of flakes and patches.
- A filter file the user may not read anyway doesn't count: rsync fails
on its own when it gets to transfer that file, and --insecure-links
changes nothing about it.
- rsync prints bytes it takes for unprintable as \#ooo, which depends on
the locale. File names get decoded before checking that they exist, so
files below, e.g., directories with umlauts in their names don't pass
for missing in the C locale.
- If rsync doesn't tell which filter files it reads at all, populate
warns that it can't tell either.
The README mentions that --insecure-links also affects the way to the
target path on local targets, and that only rsync 3.5.0 ignores filter
files below directories that may be entered but not read.
Phases of scenarios can set environment variables for populate. New
file scenarios sync a subdirectory whose filter file is above it, and a
source with a filter file below a directory with an umlaut in its name,
in the C locale.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
inputs
Delivering flakes and flake inputs gets more robust:
- With fetchOnTarget, the inputs get collected before the pipeline that
links them, so populate fails if the target can't fetch one, instead of
linking the flake without its inputs and succeeding.
- What the lock file pins gets taken from the store if it's there: the
store path is the one of a directory named source with the locked hash,
which nix-store --print-fixed-path computes. builtins.fetchTree
contacts where the input comes from even then, so redeploying unchanged
sources fails without that if a server is unreachable.
- With fetchOnTarget, a delivered input that is itself locked to a local
path the target lacks fails with populate's error naming the input, like
its inputs do.
- Inputs that get drawn from other sources are left out, with all they
pin: Nix archives an empty flake in their place. So nothing gets
fetched, copied, and rooted that no evaluation uses, and a pin that is
unreachable doesn't matter if it gets replaced anyway.
- flake-input sources get the option requireInputs. By default,
populating fails if the inputs that the input's own lock file pins
can't be delivered, e.g. because the lock file lacks an input its
flake.nix has. With false, populate warns and delivers the input with
what Nix locks now, or else without its inputs.
A remote flake that the target fetches entirely on its own collects its
inputs like everything else does. The Nix function finding its
subdirectory moves next to the jq programs.
New edge cases cover each of these.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Populate replaces what's at the target paths of sources, removes what
sources don't deliver anymore, and prunes. Nothing of that may reach
beyond the target directory, whatever names sources have and whatever the
target directory contains:
- Source names that aren't file names in the target directory don't
evaluate: the empty name, ., .., .populate, and names with slashes or
control characters. E.g. a git source named "" removes the target
directory, and a file source named .. syncs into its parent.
- Target paths that aren't absolute, that are the root directory, or that
contain .. don't evaluate.
- A symlink at the target path of a source gets replaced, not followed,
and populate says so. rsync-based and flake sources do that already.
git sources replace the link as well, and also a work tree whose .git is
a symlink or a file naming a repository elsewhere, instead of fetching,
resetting, and cleaning there. derivation sources replace a link that
doesn't point into the store, instead of failing.
- Populate refuses to use the sentinel, or a state directory therein, if
it is a symlink, instead of changing and removing what it points to.
- What populate removes recursively gets removed as a link if it is one:
chmod -R follows a link named on the command line.
- Flake and flake-input sources that get patched, get their lock file
rewritten, or are materialized replace a symlink at their target path
like every source does, and say so.
- Nix rewrites lock files in place, which changes other hard links to
them as well, e.g. in a backup or outside the target directory, and
what a lock file that is a symlink points to. Populate replaces a lock
file by a copy of its own before rewriting it.
The target directory itself may be a symlink. Nix refuses path: URLs with
symlinks on the way, so the flakes step resolves the paths it hands to
Nix.
Backups work on a sentinel that is still a file, which becomes a
directory, like for everything else that keeps state.
The tests create canaries outside the target directories: a file and a
directory beside them, and a directory elsewhere with a read-only
directory and a Git work tree with uncommitted changes. The check of
every phase of every scenario fails if a canary changed, or if a store
path that the target directory links to doesn't match its hash anymore.
A new test, safety, points links at the canaries: at the target path of
every source type, inside what sources replace, in place of populate's
state, and among what it removes and prunes. It also covers the names
and target paths that don't evaluate, a target directory that is a
symlink, and a lock file with another hard link.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Sources drawing inputs from other sources get the lock file of their
flake rewritten with nix flake lock --override-input, so the flake in
the source itself uses the other sources, and needs no generated flake
to be found. file and git sources get their lock file rewritten in
place. flake and flake-input sources get delivered as a copy with the
rewritten lock file, which gets added to the store with nix-store
--add-fixed, and the target path links to that. With rewriteLock set
to false, and for symlink sources, the source stays as it is and gets
a generated flake in .populate/flakes/<name>, which writeDeploy's flake
parameter uses.
flake and flake-input sources get two more options:
- patches: patches on the deploying machine, which get uploaded to
.populate/patches/<name> and applied in order with patch -p1 on the
target, in the directory the target path shows.
- materialize: the target path becomes a writable copy instead of a
link into the store.
Sources that get patched, rewritten, or materialized get delivered to
.populate/derived/<name>/original, which also roots the delivered store
path, and the flakes step derives the target path from there. Copies
get made again only if what they derive from changes, which a key in
.populate/derived/<name> records.
The matrices of the file, flake, flake-input, and git tests cover both
ways of drawing inputs, those of flake and flake-input materialize, the
deploy test a source with a rewritten lock file, and features and
edge cases run the scenarios drawing inputs both ways. New features
cover patches, a patch that doesn't apply, patches in a subdirectory,
reusing copies, and materializing.
|
| |
|
|
|
|
|
|
|
|
|
| |
Flake and flake-input sources take the flake reference as attribute url,
like git sources take their repository, and like inputs in flake.nix:
something.flake = "github:owner/something";
otherthing.flake = {
url = "github:owner/otherthing";
inputs.nixpkgs.follows = "nixpkgs";
};
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
rsync 3.5.0 opens merge files, including per-directory .rsync-filter
files, walking the path one directory at a time, refusing symlinks that
neither root nor the user running rsync owns, and opening each directory
for reading. It treats the files it can't open that way as missing,
without saying so. That happens, e.g., below a home directory with mode
711, which the user may enter but not read, or behind another user's
symlink. Such a source then got synced without its .rsync-filter rules:
excluded files got transferred, and --delete-excluded deleted nothing.
Before syncing with rsync 3.5.0 or later a source containing .rsync-filter
files, populate asks rsync which of them it reads: a dry run with
--debug=FILTER2. If it reports an existing one as missing, the file
source type's new insecureLinks option decides: with null, the default,
populate fails with an error naming the file; with true, rsync reads such
files anyway (--insecure-links); with false, rsync syncs without them.
Both print a warning.
File scenarios sync a source through a symlink owned by nobody, with each
setting.
|
| |
|
|
|
|
|
|
|
| |
Depending on the version of Nix, lock files name subdirectories as
attribute dir or as query parameter dir of the URL. The VM tests run
one version, so only one shape gets covered there. The jq programs for
flakes move to pkgs/populate/flake-jq.nix, and a new test, jq, runs them
without VMs over lock files of both shapes, with follows, removed and
cyclic follows, and inputs given by relative paths.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A flake-input source delivers an input as its own flake, which
evaluating it uses with its own lock file. Populate, however, took the
input's inputs from the parent flake's lock file: on the target, by
walking the lock file, and on the deploying machine, from the parent's
nix flake archive. That had several consequences:
- An empty follows path, which removes an input, or two inputs following
each other made the walk loop forever.
- Copying left out inputs that follow other inputs, and rejected names
through them, e.g. dep/inner where dep's inner follows another input.
- Inputs given by relative paths, e.g. path:./lib, which have no lock
entry of their own to fetch, broke fetching on the target and
flake-input sources.
- Where the parent overrides an input's inputs, the input's own lock file
pins other versions than the delivered ones.
Populate now resolves the name through the parent's lock file, follows
included and guarded against cycles, to what to fetch: a locked
reference, or the flake itself, which inputs given by relative paths are
part of. It fetches that, on the deploying machine or on the target, and
then the inputs the input's own lock file pins, using nix flake archive.
An input without a lock file comes without its inputs.
New edge cases cover lock files with empty and cyclic follows, names
through follows, the delivered closure, and relative path inputs.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A new test, edge-cases, covers paths that work but have no scenario yet:
a flake given by an indirect reference, which the target fetches by its
locked reference; a remote flake in a subdirectory; a flake in a
subdirectory drawing inputs from sources; a dirty Git work tree; a flake
whose lock file needs updating; drawing inputs from a source that isn't
a directory; nested names of missing inputs; several sources sharing one
flake; and an input locked to a path that the target has.
The deploying machine gets a registry entry for the indirect reference.
Resolving it fails if the global registry can't be downloaded, which it
can't in the VM, and disabling the global registry with flake-registry
needs flakes, so the deploying machine enables them, like any deploying
machine using indirect references does. The target doesn't, so checks
for flake support on targets still mean something.
|
| | |
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
| |
pkgs/krops takes populate as an argument, which nixpkgs doesn't provide,
so evaluating any of the flake's lib attributes from pkgs/krops fails.
The flake passes its own populate.
The README describes using krops from a flake: writeDeploy and the other
functions from lib.<system>, evalSource and mkTarget from krops' lib
directory, and deploying with nix run --impure, because krops' lib
evaluates <nixpkgs/lib>, environment variables, and /etc/hostname.
|
| |
|
|
|
|
|
|
|
|
|
| |
nixos-rebuild deploys /etc/nixos/flake.nix instead of NIX_PATH if that
file exists, so deploying anything but a flake fails on such targets.
Without writeDeploy's flake parameter, nixos-rebuild gets --no-flake if
/etc/nixos/flake.nix exists on the build target. nixos-rebuild versions
predating flakes neither know --no-flake nor look for that file, so they
don't get it.
The deploy VM test covers targets with and without that file.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Populate never removes what it delivered for sources that aren't
configured anymore. For flake and flake-input sources, that keeps their
garbage collector roots, so what they delivered never gets collected.
With prune, populate removes everything from the target directory that
isn't a source, except .populate, after syncing the sources. It also
removes the roots in .populate/flake-inputs of names that aren't flake or
flake-input sources, and uploads left behind by interrupted runs.
Pruning is opt-in, because the target directory may contain other things
worth keeping, or be shared by deployments with different sources.
writeDeploy, writeTest, and writeCommand pass prune on to populate.
A feature scenario deploys sources, deploys fewer without pruning, then
with pruning, and checks that the removed sources' entries and roots are
gone, while the remaining sources and .populate stay.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
tests/default.nix provides one VM test per source type, and one for
features spanning sources. Each test runs scenarios from
tests/scenarios.nix, which cover every combination of a source type's
options, deployed to the deploying machine itself, to a remote target as
root, and to a remote target using sudo. A scenario prepares the target
directory, deploys, checks the result, and deploys and checks again.
Scenarios expecting populate to fail check its error message, or check
at evaluation time if populate is expected not to evaluate.
The deploying machine creates fixtures using tests/fixtures.sh and serves
them via HTTP and git daemon, so flakes can be fetched on the target
without further network access. Checks evaluate delivered flakes without
network access to the fixtures after collecting garbage, so they verify
that the delivered store paths are complete and rooted.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
With fetchOnTarget, the target fetches the flake's inputs using the
arguments recorded in the lock file, like it does for flake-input
sources. The flake itself has no lock entry, so the target fetches it
by its locked reference, unless that refers to the deploying machine,
e.g. a path or a Git work tree. Then the flake's tree gets transferred
using rsync and added to the target's store using nix-store --add-fixed,
which yields the same store path as on the deploying machine, because
flake store paths are content-addressed by their NAR hash. Adding paths
like this needs neither signatures nor a trusted user, so this works for
targets using sudo. The transfer gets skipped if the target's store has
the flake already, and populate fails if the added path differs from
the expected one.
rsync' gets a filterFiles option, so the flake's tree gets transferred
without applying .rsync-filter files, which would change it.
|
| |
|
|
|
|
|
| |
The flake-input source type delivers an input of a flake, as recorded in the
flake's lock file, and the flake source type delivers the flake itself.
fetchOnTarget belongs to flake-input, because only a locked input can be
fetched on the target.
|
| |
|
|
|
|
| |
With flake set, nixos-rebuild deploys that flake from the build target's
source directory, with flakes enabled for the command. The deployment fails
with an error message unless Nix on the build target supports flakes.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
For each source that draws inputs from other sources, populate generates a
flake in .populate/flakes/<name>, which re-exports the outputs of the
source's flake with those inputs drawn from the other sources. The source
itself stays as it is, so NIX_PATH still refers to it.
Generated flakes refer to each other's generated flakes, so a source's own
declarations apply wherever it gets drawn from, and get generated in
dependency order. Evaluation fails for sources drawing inputs from missing
sources, from themselves, or from each other. Generated flakes get locked on
every run, and get removed once their sources no longer draw inputs from
other sources.
|
| |
|
|
|
|
|
|
|
|
| |
With fetchOnTarget, the target fetches the input and its own inputs itself,
using fetchTree with the arguments recorded in the flake's lock file. Only
nix flake metadata runs on the deploying machine, so the inputs get
downloaded once, by the target. This needs Nix with flake support on the
target, which populate checks before fetching.
Because nothing gets copied, this also works for targets using sudo.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Deliver a flake, or one of its locked inputs, to the target: archive the
flake locally, copy the selected input together with its own inputs into the
target's store, and link it at the target path, registered as garbage
collector root. The result can be used in NIX_PATH as well as by flakes
evaluated on the target, without network access.
nix copy runs with a separate ssh wrapper without the control socket, because
Nix waits for its LocalCommand, which may not run over a multiplexed
connection.
|
| | |
|
| |\
| |
| | |
Update README.md to fix broken link with infos to the `target` attribute
|
| | |
| |
| | |
fix broken link to with infos to the `target` attribute
|
| | | |
|
| |\ \
| |/
|/| |
|
| | | |
|
| |/ |
|
| | |
|
| | |
|
| | |
|
| | |
|
| |
|
|
| |
Signed-off-by: Matthias Beyer <mail@beyermatthias.de>
|
| | |
|
| | |
|
| |
|
|
| |
Because there's no need to save on characters.
|
| | |
|
| | |
|