From 6ced3d5da8080520f1aa4a5103834f33206ddc19 Mon Sep 17 00:00:00 2001 From: Thomas Walker Lynch Date: Wed, 11 Nov 2026 06:54:22 +0000 Subject: [PATCH] make hierarchy --- developer/design_AccountTree.html | 761 ------------------------ developer/tool/build_component/make | 4 +- developer/tool/build_component/makefile | 73 +-- shared/tool/makefile/Harmony.mk | 133 +++-- 4 files changed, 115 insertions(+), 856 deletions(-) delete mode 100644 developer/design_AccountTree.html diff --git a/developer/design_AccountTree.html b/developer/design_AccountTree.html deleted file mode 100644 index 8866744..0000000 --- a/developer/design_AccountTree.html +++ /dev/null @@ -1,761 +0,0 @@ - - - - - AccountTree 5.0 Design - - - - - - - - - - - -

Purpose

- -

- AccountTree keeps light containers made of Linux accounts. A person's work is divided among accounts, each with its own files and processes, so that a web browser, an email program and a development project cannot read each other's files, without the cost of a virtual machine. This document gives the design as it is currently envisioned, for the standard numbered 5.0. It covers the accounts, the storage that keeps their data, the systems the storage is attached to, the databases, the requests through which accounts and mounts are changed, the programs that make them, and login. Where a decision is still open it is marked as open. The features planned for later standards are listed at the end. -

- -

- document/doc_2.html gives the reasoning behind the flat home directories and the idmapped views. It was written in the earlier terms described below; this document builds on it. -

- -

Ontology

- -

Why the terms changed

- -

- The project was called SubU, for subordinate user, and spoke of a MasU, a master user, owning its SubUs. In Linux the word user is overloaded. It names the person at the keyboard, human or AI, and it names the account in /etc/passwd that the person uses, as in useradd and user name. The two are different things: one person uses many accounts, and an account has no will of its own. The design came to need the difference everywhere. Two accounts of one person are not the same account, while an account on a system and an account in a storage database can be one account in two places. With one word for both, every sentence about them was ambiguous. -

- -

- The MasU and SubU terms had a second fault. Some passages spoke as though there were one MasU at the top, and others as though every SubU could be a MasU. Both were true, because master and subordinate are relative, but the terms gave no word for the top, or for an account with nothing below it. -

- -

- So the word user is dropped. A person is the agent; an account is the account. The structure is described in the terms of a tree, which are relative where they should be and fixed where they should be. The project is renamed AccountTree, for what it keeps, and SubU and MasU are retired. The word user survives only in names the project does not own: uid, which is short for user id, useradd and userdel, and the kernel's user namespaces. -

- -

The account tree

- -

- Every account on a system has one parent, and root is at the top. In Linux root already administers every account, so making root the top of the tree describes what is already so, and the tree fits into Linux without changing its shape. Root keeps its one meaning. -

- - - -

- Every account is entered through its parent. Root enters its child accounts through the system's login programs, the display manager, login at the console and sshd, which run as root and start a session for a person who gives a password or a key. A login account enters its child accounts through AccountTree's request gate, which starts sessions and programs in them; an account below a login account has no password, and the login programs do not enter it. The rule is the same at every level of the tree. Only the means differ, and the means for root's children are the ones Linux already has. -

- -

- A parent account reaches a child account's files through a view. -

- - - -

- An account's name is its parent's name, the middle dot, and its own short name: Thomas·developer·scratch is a child of Thomas·developer, which is a child of the login account Thomas, which is a child of root. In identifiers the wider category comes first, so prose says child account and parent account, and code says account_child and account_parent. -

- -

Term

- - - -

Account and name

- -

Account name

- -

- The middle dot (U+00B7, written ·) is not permitted in a short name, so a full name divides in exactly one way, and the conventional characters _, - and . remain free for use in names. It lies outside the POSIX portable character set. Making an account needs the --badname flag of useradd, and a tool that checks account names against the portable set needs its check widened. In UTF-8 the middle dot is two bytes, and an account name is currently limited to 32 bytes. That limit is set in user space, by the fixed name field of the utmp login records and by useradd, not by the kernel, which knows accounts only by uid. pkg-config prints paths holding the middle dot in a form that is not valid UTF-8; builds that pass such paths through pkg-config need the workaround given in shared/project-dependency/Python.org. -

- -

- The name AccountTree is reserved, because on a running system /home/AccountTree holds AccountTree's own files. A system account made for a branch of a storage database is named with a tilde and eight hex digits, as Thomas·try~a7b3247f, described under Copies. That is a default, not a rule on names: the system map, not the name, records which system accounts belong to a branch. -

- -

Identity of an account

- -

- A storage account has an identity in its storage database, and, when the storage is attached to a running system, an extended identity on that system. -

- -

- In its storage database, an account is identified by the database uuid together with its database uid. Its files on the storage are owned by that database uid, and root, mounting the storage by hand without AccountTree, sees that database uid on them. This pair is all the storage database needs to identify the account. -

- -

- On a running system, the account also needs a system uid, because that numbering is the one /etc/passwd and the kernel use there. A row of the system map gives it: the row pairs the storage account's identity with a system account. The account's extended identity is its identity in its storage database, joined through the system map to the system. -

- -

Storage and system

- -

Storage layout

- -

- Every storage has the same layout under its root, where the root is the directory the storage is mounted at: / for the disk a system runs from, or a mount point such as /mnt/Eagle for a drive plugged in. Its tree dir is home/AccountTree under the root, so /home/AccountTree or /mnt/Eagle/home/AccountTree. -

- - - home/AccountTree/ the tree dir: root's, mode 0711 - home/AccountTree/database/ the database account's, mode 0700 - home/AccountTree/database/storage.sqlite3 the storage database - home/AccountTree/account_data/ root's, mode 0700 - home/AccountTree/account_data/<name> the home of each storage account, by name - home/<other> anything else, such as the homes of unmanaged accounts - - -

- Everything AccountTree keeps on a storage is under its tree dir, so the storage can carry other things beside it, and the storage database sits with the data it describes. account_data holds the accounts' homes; on a running system each is reached through an idmapped mount at /home/<system account>. An account named database cannot collide with the database, because the two are at different levels. -

- -

- The ownership divides the tree dir between the two accounts that act on it. SQLite writes a journal file beside a database while a transaction is open, so the account that writes a database needs to write the directory it is in; the database is therefore in a directory of its own, database/, which the database account owns. Everything else is root's. The database account can pass through the tree dir, mode 0711, to reach its directory, but cannot enter account_data, so it cannot rename, move or read a home. A root interpreter that checks a home's owner on disk is then checking something the database account could not have arranged; see Answer check. -

- -

- On the disk a system runs from, AccountTree's files are therefore under /home, beside the homes of unmanaged accounts. Distributions already keep /home apart from the operating system: openSUSE on btrfs makes it a subvolume outside the root snapshots, so a rollback of the root leaves it alone, and installers on other file systems commonly offer it as a partition of its own, kept when the system is reinstalled. AccountTree's files inherit this without depending on any file system. Were they on the root file system, a rollback of the root would roll back the storage database and the accounts' files with it. -

- -

- A tool that walks /home sees each mounted account's files twice: at /home/<system account>, and at /home/AccountTree/account_data/<name>. A bind mount of the same file system reports the same device number, so options such as rsync -x do not stop at the mount. A backup of /home excludes one of the two. -

- -

Every storage is the same

- -

- A storage is a repository of accounts' data, whether it is built into the machine or plugged in, and a machine can have several. The SSD inside a laptop is attached at boot, before any login, in the same way a USB drive is attached when it is plugged in. Taken out of the laptop and put into a USB enclosure, the same SSD can be attached to another machine, and its accounts used there. -

- -

Copy and branch

- -

- The database uuid names the database, not the storage it is on. A copy of a tree dir, made for a backup or an archive, carries the same database uuid, and is the same database until one of the two is changed. Because a tree dir can be anywhere, an archive kept at /<storage name>/<date>/home/AccountTree can be attached by giving that tree dir. -

- -

- A storage moved between systems, plugged into a desktop, then a laptop, then the desktop again, is the ordinary case. A copy used apart from its original is the case to notice: a storage restored from a backup after the first was lost, or a lost drive found again after work went on on its replacement. The record of transactions tells these apart, as git's history does: -

- - - -

- At attach, the storage's record is compared with those heads: -

- - - -

- A storage gone backwards, sideways or unrelated is attached with a warning, and the warning and the kind are recorded. No one is kept from working: a person restoring from a backup attaches the restored storage and goes on, and its line becomes the one this system follows. Merging the two lines afterwards is done by hand in 5.0. -

- -

- The two lines can also be kept apart, so that both can be attached at once and their files compared. attach <tree dir> branch makes the storage a branch: it gives the database a new database uuid, records in it the database it came from and the head they parted after, and makes a system account for each storage account that had one on the line it came from, named <system account>~<eight hex digits>. The eight digits begin the hash of the transaction that made the branch, which no other branch has, and which does not change as either line grows. The branch is then a storage database in its own right. -

- -

- The same database attached twice would mount two homes at one place. When a storage's homes are already mounted from another copy of it, attach mounts nothing, and says to detach the other copy or attach this one as a branch. -

- -

- Bringing two lines together again is a merge. For the storage database, a merge replays the transactions of one line onto the other where they do not conflict. For the accounts' files, it is the integration problem of 7.0, of which merging two lines of one storage is the simplest case. -

- -

- These checks establish continuity, not authenticity. The hashes are not secret: they are in the storage database and the system database, and anyone who can read either can copy them into a fabricated database. Authenticity needs a secret, which is the subject of the keys planned for 6.0. -

- -

Uids are a storage database's own

- -

- The uids in a storage database are that database's own numbering. Files on the storage carry them. A storage database never adopts the uids of a system it is attached to; its accounts are mounted onto the system through idmapped mounts, which translate database uids to system uids as the files are used. A storage can therefore be moved from machine to machine without changing its files. -

- -

- A storage database gives out database uids counting down from 1999999999, while systems give out system uids counting up from low numbers. A database uid is therefore not also the system uid of an account, and a file on a storage, seen without its mount, does not appear to belong to a system account it does not belong to, except in the cases given under Limit of a uid. The band 1879048192 to 2147352575 is left unused by systemd's allocation of uids, so a storage database has about 120 million database uids before it leaves the band. An account brought into AccountTree from an existing one can keep its existing uid, with the keyword uid of account make; for that account the coincidence remains, behind the root-only directories of the tree dir. -

- -

- A storage database never reuses a database uid. A database uid given up by a deallocated account stays retired, because files carrying it can remain on the storage, in a backup, or on another storage they were copied to. -

- -

Limit of a uid

- -

- Two numberings meet on a running system: database uids, on the files of storage, and system uids, in /etc/passwd. Each has limits, set by different parties, and each limit is checked where the party that sets it is known. -

- -

- The kernel. Linux uids are 32 bits. 4294967295 is (uid_t)−1 and means no uid; 65534 is the overflow uid shown for a uid that cannot be mapped; neither is given out. A database uid is never used as a system uid. At run time it meets only the idmapped mount, which accepts any uid below 4294967295, so the kernel's limit is the same on every system and a storage moved between systems is unaffected by it. -

- -

- The file system. ext4, xfs and btrfs store 32-bit uids. JFFS2 and Minix store 16 bits, and FAT and exFAT store none; such storage cannot carry AccountTree accounts. This limit belongs to the storage and travels with it, so it is checked once, when the storage is made: storage make sets the owner of a test file to the first database uid, reads it back, and refuses the storage if the two differ. That check is not yet built. -

- -

- The system. UID_MAX in /etc/login.defs bounds only the uids useradd picks on its own; AccountTree gives the system uid. A system's range of system uids is set by system make, 2000 to 59999 by default, and is the system's own. A storage moved to a system with a smaller range keeps its database uids, since they never become that system's uids; the smaller range limits only how many of the storage's accounts the system can make system accounts for, and is-same-account reports when the range is used up. -

- -

- Where the two numberings can still meet. A database uid equals the system uid of some account when: -

- - - -

- At attach, the database uids of the storage are to be compared with the system uids in use, from the system's account database, which includes directory services, and from /etc/subuid. Each coincidence is logged with the system account that would appear to own the raw files. A coincidence is not refused: through the mounts each account sees its own files correctly, and the raw files are behind account_data, which only root can enter. It is reported because a tool that works by uid on the raw files, run as root, would treat them as that account's. This check is not yet built. -

- -

- Tools that copy files. Backups and archives copy uids with the files. The old ustar format of tar holds uids up to 2097151, and the odc format of cpio up to 262143; database uids do not fit. GNU tar's default format, the POSIX pax format, cpio's newc format, rsync and btrfs send hold 32-bit uids, and archives of AccountTree storage use one of them. -

- -

The system

- -

- A system is a party in its own right, with its own database. Its system database, at /var/lib/AccountTree/system.sqlite3, holds its system uuid, the range of system uids AccountTree gives out, the system map, every head of each storage database it has seen, and the limits. The system owns the uid numbering of /etc/passwd: it alone decides which storage accounts get system accounts on it and with which system uids, so that record is its own and not any storage's. A storage plugged in can claim nothing; what it names in its own database grants it no system account. -

- -

- A system never reuses a system uid. Its own disk keeps files carrying system uids, in /tmp, /var/spool, crontabs and logs, and a reused uid would hand them to another account. The system map therefore keeps every row it has held, retired rows included, and a system account AccountTree makes is never given a system uid that appears in it. -

- -

- The system database is under /var, which holds the state programs keep, and not under /etc, which holds configuration. A system whose root file system is snapshotted, as openSUSE's is, keeps /var outside the snapshots. A rollback of the root then rolls back /etc/passwd and leaves the system map as it was: a system account made after the snapshot is lost from /etc/passwd, but its system uid stays recorded, is not given out again, and the system account can be made again from the system map. Were the system database under /etc, the rollback would also forget the system uid, and it could be given to another account while files carrying it remained under /var. -

- -

Deleting storage

- -

- AccountTree_admin storage delete <tree dir> undoes, on this system, what AccountTree did for the accounts of the storage database, then deletes the tree dir. It makes every check first, and a failed check changes nothing: -

- - - -

- For each row of this system's map naming one of its accounts, it then unmounts the home, deletes the mount point attach made, recognized as an empty directory owned by root with mode 0000, deletes the system account and its group when AccountTree made them, keeps an adopted system account, and retires the row. Last it deletes account_data, the storage database and the tree dir. Run a second time, it finds nothing to delete and says so. system delete deletes the system database, and is refused while any row of the system map is in use. -

- -

Database

- -

- There are two kinds of database, with two schemas, schema_storage.sql and schema_system.sql. Both keep the same record of changes, described below, and are checked the same way. Each fact is kept by the party with authority over it: a storage database keeps its accounts and their database uids; the system database keeps which of those accounts are the same accounts as system accounts. -

- -

Storage database table

- - - -

System database table

- - - -

Series number and hash chain

- -

- A transaction is one change to one database: the SQL statements a command of AccountTree_admin, or a request, runs against that database, recorded together. A command can make transactions in both kinds of database; is-same-account makes one in the storage database and one in the system database. -

- -

- Each transaction increments the series number, and records its statements, a description, a random number, and a hash. The hash is taken over the previous transaction's hash, the series number, the description, the statements, and the random number. The first transaction follows a hash made from the database's kind and the uuid it was made with, so no two databases' chains can coincide, and a record cannot be moved from one database into another. A branch keeps its record, and the transaction that made it a branch is in that record. The identity row holds the series number and hash of the last transaction, the head of the chain. -

- -

- The chain gives order, integrity and identity of history. It does not give authenticity. Anyone holding a database can extend its chain, or rewrite it and compute new hashes. What a rewrite cannot do is keep the hashes that others have already seen: a system that remembers a head finds that the rewritten record does not hold it. Each party that remembers a head is a witness. A storage database and the system database are readable only by the database account and root, so the record is the database's own history, as a git repository's history is, rather than a log for persons to read. -

- -

- The series number orders a database's history; the hash identifies it. A copy that is used goes on to make its own transactions, with the same series numbers as the original's and different contents. Their hashes differ, because the random number differs even when the same change is made to both. So two databases with the same database uuid and series number but different heads are known to have parted. -

- -

- Times recorded in a database are for people to read. They are not authoritative, because clocks differ between machines. The series number is authoritative. -

- -

Standard

- -

- A program that opens a database first reads its standard table, and refuses a database that follows no standard the program follows. During a change from one standard to the next, a database lists both. -

- -

Fallback tool

- -

- The databases can be built by hand from SQL scripts kept in the project. They are the means of starting over if a database is lost or damaged. -

- - - -

Same account

- -

- A storage account's home, account_data/<name> in its tree dir, carries only its database uid. What lets it be used on a system is a row of that system's map, saying that a system account there is the same account as the storage account. The administrator states it: -

- - - AccountTree_admin is-same-account system <host name> <system account> storage <tree dir> <storage account> [make-system-account] - - -

- A keyword such as make-system-account is a word given after a command's other arguments, as the command itself is a word, not a flag. The two account specifiers are accepted in either order. In 5.0 the pair is a system account and a storage account; correspondences between two storage databases come later. The host name must be this system's, which confirms that the administrator is on the machine he means. When the system account exists, it is adopted: it must be an account of a person, with a system uid at or above UID_MIN, its home must be /home/<system account>, and it must not already be the same account as another storage account. When it does not exist, the keyword make-system-account makes it, with a system uid from the system's range that no account, no group and no row of the system map has had, provided /home/<system account> is absent or empty, since the new system account would take whatever is there as its home. Without the keyword the command refuses, so that a mistyped name does not make a system account. -

- -

- The first time a storage account is made the same account as a system account on a system, the storage database records that system in system_seen. That and the system map row together are what 5.0 calls an introduction; in 6.0 it is also when keys are exchanged. account make with make-system-account makes a storage account and, in the same command, a system account of the same name, the same account as it. -

- -

- is-not-same-account undoes it: it unmounts the home if it is mounted from that storage, deletes the mount point attach made, deletes the system account if AccountTree made it and keeps an adopted one, and retires the row. same-account show lists which accounts are the same, with the storage accounts and the persons' system accounts that are not. same-account suggest prints is-same-account commands proposed by matching names, for the administrator to read and run; it changes nothing. Matching names is a guess, and is used nowhere else. -

- -

- Someone else's storage, plugged into this system, gets nothing until the administrator makes its accounts the same accounts as system accounts here. There is no guest attach in 5.0: every storage is introduced by the administrator. A storage account named Thomas on that storage is no one here until the system map says so. -

- -

- Two storage databases can record that an account of one is the same account as an account of the other, and can record it while a system's map says otherwise. Such a claim grants nothing until a system acts on it, as in mounting a parent account's view of a child account kept on other storage. A system acts only on its own record: it does not infer that two accounts are the same from what storage databases say, and where the claims of storage databases and its own map disagree, it reports the disagreement. A child account made through a request on this system was made by this system, which records the parent relation itself. -

- -

- In 5.0 storage databases and systems recognize each other by database uuid and system uuid alone. A copy of a storage database is taken for the original, and gains the system accounts mapped to the original's accounts. That is enough among one person's machines and storage. It is not enough among strangers, because either kind of uuid can be copied; keys are given under standard 6.0. -

- -

Request

- -

No server

- -

- A person's program asks for what it wants by composing a journal and invoking a gate with it: AccountTree's program is its own Man_In_Grey orchestrator, and no server stands between the person and the gate. The gate's real uid is the account that asked, reported by the kernel, so the guard knows who asks as a fact rather than as a claim in the journal. The place where authority is decided and the place where privilege changes are then one place, the gate, which is what Man_In_Grey was built to be. -

- -

- An earlier form of this design had a server, running under an account of its own and receiving requests on a socket. The server learned the requesting account from SO_PEERCRED, but every journal it composed reached a root gate with the server as the gate invoking account, so the root guard would have had to trust the server's word for who asked. The server also wrote the system database, while the root guards checked their constraints against that database, so a compromised server could have written a record and then asked root to act on it. Taking the server away removes both. -

- -

- A gate is a function call. The program that invokes it waits, and is given back the gate's exit status and, on descriptor 3, the interpreter's result. A request's answer is therefore tied to the request by the kernel: it arrives on the descriptor of the gate the program started. -

- -

The gate

- - - -

- The administrator's gate needs no setuid bit, since root invokes it already holding root's rights, and without the bit the administrator's instructions have no setuid program to reach them by. It is a gate all the same, so that the administrator's journals pass a guard as a person's do, and the code that changes the system lives in one place, the interpreters. Making accounts the same, attaching, detaching and setting limits all go through it, at boot as at any other time. -

- -

The database account

- -

- The databases are opened by the database account and by no root process. A storage database is input from a drive: a copy, a branch, or a drive found again after it was out of its owner's hands, whose file anyone could have rewritten. Opening it means parsing its pages and its schema and walking its whole hash chain, and SQLite's own guidance treats a database file from an untrusted source as an attack surface. Under the database account, a fault that a crafted database reaches gives that account, not root. This is privilege separation in the manner of OpenSSH: the complex parsing of untrusted input runs unprivileged, and the privileged program does a few simple things with what it is told. -

- -

- The database account owns /var/lib/AccountTree, where the system database is, and on each storage the tree dir's database/ directory. Its interpreter opens the databases with SQLite's defensive settings, trusted_schema off among them. -

- -

Who is asking

- -

- The request gate records the gate invoking account, its real uid, for its guard and interpreter, as every Man_In_Grey gate does; the account cannot misstate it. That account's name is the prefix of any child account it makes. At the record gate the gate invoking account is root, and the journal names the requesting account as an argument. The record guard trusts that argument because the account that gave it is root, which could do anything anyway; its constraints allow only root to give it journals. -

- -

The request language

- -

- The request language is made of requests, not of the primitive steps that carry them out. A journal that asked for a system uid to be made, or a path to be mounted, would leave the guard to work out what request the steps add up to, and to check every uid and path the composer chose against records it would have to consult again. A request names the work, and the uids, paths and mount options are the interpreter's to choose; none of them can be expressed in the language. -

- -

- Each request acts on the requesting account or on an account in the tree below it, with the authority of the requesting account; this is how a child account's window, opened from its parent's, acts with the parent's authority. Each has a result, which the person's program shows. -

- - - -

- reconcile, which compares the databases with /etc/passwd and the mount table, completes what an interrupted request left undone, makes again any system account lost from /etc/passwd, and reports what differs, is an instruction of the administrator's language, run at boot and on demand. -

- -

A request's course

- -

- For make_child_account: -

- -
    -
  1. The person's program composes a journal of the one request, and invokes the request gate with descriptor 3 open.
  2. -
  3. The request guard approves it: the journal is well formed, the gate invoking account is a person's account, with a system uid at or above UID_MIN, and the short name is valid. Whether the account is managed, and what it is allowed to do, are questions for the records, asked next.
  4. -
  5. The request interpreter composes a journal for the record gate, naming the requesting account and the short name, and invokes it.
  6. -
  7. The record interpreter checks the tree and the limits against the records, gives out the database uid and the system uid, each within its database's write transaction, records the account and its intended state, and answers with the name, the two uids, and the series number and head hash of each transaction.
  8. -
  9. The request interpreter checks the answer, Answer check below, then makes the system account, the home, the mount point, the mount and the parent's view.
  10. -
  11. The request gate hands back the result: the account's name and uids.
  12. -
- -

- A refusal from the record interpreter, for a limit for instance, comes before anything on the system has changed, since the records are asked first. The series numbers and head hashes in the answer are logged with the changes the request interpreter makes, so that every change to the system is tied to the database transaction that recorded it. -

- -

Giving out a uid

- -

- Each database gives out its own uids, and its write lock is what keeps two requests from being given the same one. The choice of the next uid is made inside the write transaction, opened with BEGIN IMMEDIATE, which takes the lock before anything is read; a uid chosen before the transaction opens could be chosen by two requests at once. The code as it stands chooses first and opens the transaction after, in account make and in is-same-account, and is to be changed. -

- -

- One check stays outside the lock: that no account made by hand already has the system uid. An account made with useradd between the record interpreter's choice and the request interpreter's useradd -u would hold it. useradd then fails rather than making a duplicate, the request fails, and since the system database recorded the uid first, reconcile finds the request half done. -

- -

Answer check

- -

- The request interpreter trusts the database account less than it trusts itself, so it checks each answer the way a guard checks a journal, against what it asked and against facts the database account cannot forge: -

- - - -

- The third check is the one that matters most. An answer that gave one account another's database uid would otherwise mount the second account's files as the first's. -

- -

Instruction language and constraint

- - - -

- The constraints that limit what root's interpreters do with what they are told stay with them: a new system uid lies in AccountTree's range and appears in no row of the system map; a system account is deleted only when the system map records that AccountTree made it; a home is mounted only from <tree dir>/account_data/<storage account> onto /home/<system account>, with the uid translation of the row of the system map that pairs them; a view is mounted only of a child account's home, at ~/account_child/<short name> of its parent; every mount is nosuid and nodev; a mount point is an empty directory owned by root with mode 0000, made and deleted only at /home/<system account>; a home is deleted only during the deletion of its storage, or of a deallocated account. -

- -

- A single request touches /etc/passwd, a home and a mount, so the request gate needs every part of root's rights those take, and the separate account, home, mount and session languages of the earlier form of this design are folded into it. The loss is small: the right to mount, CAP_SYS_ADMIN, is nearly root by itself. The account, home, mount and session code stays in separate modules within the request interpreter. -

- -

Partial failure

- -

- A request makes several changes. The journal for a request is approved whole before any instruction executes, so a request that breaks a constraint changes nothing on the system, and the record interpreter's refusals come before the request interpreter changes anything. A step can still fail while executing, after earlier steps succeeded. The databases record the intended state of each account, so such a failure leaves the intended state recorded and the system short of it; reconcile finishes the work or reports why it cannot. A request is complete only when the system matches the databases. -

- -

At boot

- -

- attach and reconcile are run at boot, before logins are allowed, by a systemd unit that runs AccountTree_admin under root. Two kinds of systemd option are to be left out of that unit, because each would quietly stop the work: -

- - - -

Limit

- -

- Making a child account takes a database uid from a storage database, and a system uid and a system account from the system, and the system's are finite. Without a limit, one account could take all of them. Each system account has a limit on the number of accounts in its tree below it, counting accounts at every depth and counting deprecated ones. Limits are kept in the system database. The administrator sets them with AccountTree_admin limit set. A system account with no limit set has a limit of 0, and cannot make child accounts. -

- -

- A child account made by a child account counts against the limits of every account above it. The starting value suggested for a login account is 100. -

- -

Mount

- -

Idmapped mount

- -

- Every AccountTree mount is an idmapped mount: a bind mount that carries a translation of uids, applied by the kernel to every permission check and to every file made through it. Idmapped mounts need Linux 5.12 or later, and a file system that supports them, such as ext4, btrfs or xfs. An idmapped mount is kernel state, with no process behind it, so it survives the end of the program that made it and of any session. A mount through FUSE, such as bindfs, does neither. -

- -

- The request interpreter, and the administrator's, make the mount with the kernel's own calls, not by running mount. open_tree clones the source as a mount not yet attached anywhere; mount_setattr gives it nosuid, nodev and the idmap; move_mount places it. The kernel is given the idmap as a user namespace: a child process is made in a new user namespace, its uid and gid maps are written, and the namespace is handed to mount_setattr. A map line reads <id inside> <id outside> <count>; for an idmapped mount the kernel takes the uid on disk, the database uid, as the id inside, and the id outside, the system uid, as the uid seen through the mount. -

- -

Attaching storage

- -

- AccountTree_admin attach <tree dir> verifies the storage database's standards and hash chain, compares its record with the heads seen here, as described under Copy and branch, then reads this system's map for the rows naming that database uuid, and nothing else. For each, it checks that the storage account is active and that the system account exists with its system uid and with its home at /home/<system account>, and mounts the storage account's home there, translating the database uid to the system uid. Storage accounts with no row are logged as skipped. The mounts are nosuid and nodev, since a storage can carry set-user-id programs and device files. Files on the storage owned by database uids the mount does not translate appear owned by nobody. -

- -

View

- -

- A parent account sees each of its child accounts at ~/account_child/<short name>: an idmapped mount of the child account's home that translates the child's uid to the parent's. Files there appear to be the parent's, and files the parent makes there belong to the child. The directory is named for the property each of its entries has, being a child account, so the name is singular. Views are set when storage is attached, for the parent accounts already in session, and at each session's start, through set views. Views are designed in outline only, and come next after this document's other parts are built. -

- -

Mount point

- -

- A mount point must exist before a mount, so /home/<system account> exists for every known account, attached or not. While nothing is mounted on it, it is an empty directory owned by root with mode 0000, and marked immutable. No account can enter it, and nothing can be written into it to be hidden later by the mount. -

- -

Login and entering

- -

- A login account whose storage is not attached has no home. A login into the empty mount point finds no initialization files, and does not read them later when the storage arrives. Login is therefore refused for a managed login account whose home is not mounted. -

- -

- The check is in PAM, which every way of logging in passes through: the display manager, the console, ssh, su and machinectl shell. -

- - - account sufficient pam_succeed_if.so uid = 0 quiet - account required pam_exec.so quiet /usr/local/lib/AccountTree/home_present - - -

- home_present allows the login when the account is not managed by AccountTree, or when /home/<name> is a mount point, and refuses it otherwise. The first line keeps root able to log in whatever the check does. -

- -

- An account below a login account is entered through its parent: the parent gives an enter request to the request gate, whose interpreter allows the session only when the gate invoking account is above the account in the tree. The interpreter hands the session to systemd to start, and returns: the gate waits for its interpreter's verdict, so the interpreter cannot be the session itself. Whether such an account is ever to be entered through the login programs as well is open. -

- -

- A session, when it starts, gives a set_views request. The gate knows the account from its real uid, so a session can set only its own views. -

- -

Program and module

- - - -

- Each program keeps the RT convention of a command line interface apart from its work functions, and the modules are imported as libraries: a window or a test calls the functions directly, never the command line. There are two layers, and one boundary between them, the gate. The person's functions, and the administrator's, compose journals and invoke a gate; AccountTree, the window and AccountTree_admin import them. At the gate the kernel says who asks, privilege changes, and the guard decides, by the constraints, what the privilege is used for. The interpreters hold the functions that change the system and the records. -

- -

- The modules follow Man_In_Grey's naming: a module is a namespace, a file named for a namespace alone is its main module, and a file specific to it carries the namespace before a middle dot. Each of AccountTree's three instruction languages is a namespace whose files parallel Man_In_Grey's own: -

- - - Request·Language.py the request language's definition - Request·Guard.py its guard, with its constraints at its top - Request·GuardCheck.py its checks for the guard - Request·Interpreter.py its interpreter - Request·LibraryInstruction.py the implementation of its instructions - Record·... the same five for the record language - Admin·... the same five for the administrator's language - - -

- The code that does the work is in modules of its own, which the libraries of instructions import. Where a direct call is not yet written, a wrapper function stands in its place, named for what it does here and commented with the direct call it will become. The names are proposed: -

- - - -

- In the code as it stands, AccountTree_admin calls the changing functions directly, as root, and opens the databases itself. Moving the changing functions into the interpreters, and the database work into the record interpreter, is the work this design calls for. Keeping the changing functions small and their calls direct also lets them later be moved into the kernel, where an account tree with root at its top fits Linux as it is. -

- -

Open question

- - - -

Road map

- -

- Standard 5.0 is the design above. Later standards add the following, each described in its own section. A database lists the standards it follows in its standard table, so a database moves to a later standard by listing it, and a program that follows only an earlier one refuses it. -

- - - -

- Guests. A person who plugs storage into someone else's machine and works there, with no administrator introducing it first, makes the machine a display with a processor for whatever storage is brought to it. The guest's storage accounts would be given system accounts at attach, in a band of system uids of their own, recorded in the system map as transient, and deleted at detach, with no rights to anything of the machine's owners. In 5.0 every storage is introduced by the administrator. Guests wait until keys, automatic attach, and the protection of a machine from what a stranger's storage carries, are understood. -

- -

Standard 6.0

- -

Longer account name

- -

- Account names built from a chain of parent names grow long, and 32 bytes, with two for each middle dot, is soon reached. 6.0 is to allow names of 128 bytes. The limit is in user space: the login records and useradd. Distributions are replacing the utmp records with wtmpdb and lastlog2, which have no fixed name field; openSUSE has done so. What remains is the check in useradd, which goes away when AccountTree edits the account files itself. Whether a system already accepts longer names is found by making a test account with a name past 32 bytes. -

- -

Key

- -

- In 5.0, databases and systems recognize each other by database uuid and system uuid. 6.0 adds two uses of keys. -

- -

- Unlocking storage. A storage's file system is encrypted with LUKS2, which keeps up to 32 key slots, any of which opens it. Each person whose accounts are on the storage has a slot, opened by a passphrase or by a hardware token such as a FIDO2 key. When a storage is introduced to a system, the administrator opens it once, and the system adds a slot of its own, with a key it keeps, so that the storage is opened and attached on that system without asking anyone for a password. systemd-cryptenroll already enrolls such slots, and can seal a key in the machine's TPM, the security chip on its motherboard, so that the key is useless away from that machine. A slot can be removed from any machine that can open the storage, which is how a stolen laptop is cut off from it. Opening a storage with a key the system enrolled is also the proof that it is the storage that was introduced, which uuids and hashes cannot give: a fabricated storage cannot be opened with that key. The keys a system keeps make the place they are kept a target in the way /etc/shadow is; sealing them in the TPM is the protection. The disk inside the machine can be treated the same way, its /home opened by the system's own slot at boot, which keeps every storage alike. -

- -

- Signed heads. Each database and each system also holds a key pair. Each transaction's hash, which covers every transaction before it, is signed with the private key of the party that wrote it, so that a head can be shown to have been written by whoever holds that key, and not only to follow from the heads before it. The signature is checked with the public key, and the first is-same-account between a system and a storage database records each one's public key in the other's database, alongside its database uuid or system uuid. On attach, each side checks that the other holds the private key matching the public key recorded for it, so a copied database uuid or system uuid alone is refused. A storage is not a running party, so a database's private key is a file on its storage, and a full copy of the database copies the key too; protecting the key, for example inside the storage's encryption, is part of the 6.0 design. -

- -

- The migration touches only the identity and acquaintance tables and the programs that read them. A 6.0 database adds the key columns and lists both 5.0 and 6.0 in its standard table while keys are exchanged. The system map, uids and the account table are unchanged, because an account is still identified by its database and its database uid. -

- -

Deduplication

- -

- A storage gathers copies of the same files, from old archives and from copies made between accounts. On btrfs, two files with the same content can be made to share their extents, after which the content is stored once while the files stay separate: a later write to one of them is written apart from the other. The kernel call that does this, FIDEDUPERANGE, compares the two ranges byte for byte before sharing them, so a false match on a hash cannot corrupt data. Tools such as duperemove and bees find candidates by hashing and leave the comparison to that call. -

- -

- A background pass over attached storage, started by a systemd timer through the administrator's gate, does the work. The pass keeps an index of content hashes, gives the kernel call the pairs whose hashes match, and so keeps each storage compacted as it is used. On a file system without shared extents, the pass reports duplicates and changes nothing. The same index serves integration in 7.0. -

- -

Deep freeze

- -

- A person's accounts, archives included, do not always fit on one portable storage: 500 GB, say, against more data than that. Most of it is seldom read. Deep freeze divides an account's home into a working part and a frozen part by access time. Files not read for a long time move to the frozen part, which is kept on other storage, and a copy to a storage that cannot hold all of it takes the working part only. Whether the frozen part's directory tree stays visible, with its content brought in when its storage is attached, is open. -

- -

- Access times are recorded only when a file system is mounted with atime or relatime. relatime, the Linux default, updates a file's access time at most once a day, which is enough here; under noatime nothing is recorded, and deep freeze cannot work. Bringing a working part back after it was changed elsewhere is the integration problem of 7.0. -

- -

Standard 7.0

- -

Integration

- -

- Two kinds of integration are needed. The first brings the files of old archives into the current accounts. The old archives have older, less mature directory structures, and the aim is to place their files in the current structure, not to keep the old structures. The content-hash index kept for deduplication shows which old files already exist in the current tree, under any name, and leaves the rest to be placed. -

- -

- The second brings back a child copy: part of an account's home copied to a portable storage, changed there, and returned. Its changes are merged into the original. A change to the directory structure on either side is the hard case, as when a file is moved on one side and changed on the other. Two lines of one storage, described under Copy and branch, are merged the same way. -

- -

Property file system

- -

- Both kinds of integration are hard because a file has one place in one tree. A file system organized by properties, or tags, would let a file belong to an old structure and a new one at once, and make a directory a query. AccountTree's contract with storage is that an account's home is a directory mounted at /home/<system account>, and the mounting is one function in the code. A home provided by a property file system changes that function and nothing else, provided the file system supports idmapped mounts or translates uids itself. At that point the AccountTree project is expected to fork. -

- -
- - diff --git a/developer/tool/build_component/make b/developer/tool/build_component/make index 30fb82d..82df53f 100755 --- a/developer/tool/build_component/make +++ b/developer/tool/build_component/make @@ -12,4 +12,6 @@ export KMOD_SOURCE_DIR="authored/$NAMESPACE_DIR" export LIBRARY_DIR="scratchpad/made/$NAMESPACE" export MACHINE_DIR="scratchpad/made/$NAMESPACE" -/bin/make -f tool/build_component/makefile "$CMD" +# Harmony.mk is the makefile run; it includes tool/build_component/makefile, +# the project's settings, and the namespace's *.mk files. +/bin/make -f "$REPO_HOME/shared/tool/makefile/Harmony.mk" "$CMD" diff --git a/developer/tool/build_component/makefile b/developer/tool/build_component/makefile index 7914c0b..ed4ec4a 100644 --- a/developer/tool/build_component/makefile +++ b/developer/tool/build_component/makefile @@ -1,55 +1,20 @@ # developer/tool/build_component/makefile -.SUFFIXES: -.EXPORT_ALL_VARIABLES: - -RT_MAKEFILE_DP := $(REPO_HOME)/shared/tool/makefile - -.PHONY: usage -usage: - @printf "Usage: make [usage|version|information|all|lib|CLI|kmod|clean]\n" - -.PHONY: version -version: - @printf "local ----------------------------------------\n" - @echo tool/makefile version 2.0 - @printf "Harmony.mk ----------------------------------------\n" - @$(MAKE) -f $(RT_MAKEFILE_DP)/Harmony.mk version - @printf "target_kernel-module.mk ----------------------------------------\n" - @$(MAKE) -f $(RT_MAKEFILE_DP)/target_kernel-module.mk version - -.PHONY: information -information: - @printf "local ----------------------------------------\n" - -@echo CURDIR='$(CURDIR)' - @echo REPO_HOME="$(REPO_HOME)" - @echo NAMESPACE="$(NAMESPACE)" - @echo C_SOURCE_DIR="$(C_SOURCE_DIR)" - @echo KMOD_BUILD_DIR="/lib/modules/$(shell uname -r)/build" - @echo CURDIR="$(CURDIR)" - @printf "Harmony.mk ----------------------------------------\n" - @$(MAKE) -f $(RT_MAKEFILE_DP)/Harmony.mk information - @printf "target_kernel-module.mk ----------------------------------------\n" - @$(MAKE) -f $(RT_MAKEFILE_DP)/target_kernel-module.mk information - -.PHONY: all -all: library CLI kmod - -.PHONY: library lib -library lib: - @$(MAKE) -f $(RT_MAKEFILE_DP)/Harmony.mk library - -.PHONY: CLI -CLI: - @$(MAKE) -f $(RT_MAKEFILE_DP)/Harmony.mk CLI - -.PHONY: kmod -kmod: - @$(MAKE) -f $(RT_MAKEFILE_DP)/target_kernel-module.mk kmod - -.PHONY: clean -clean: - @printf "local ----------------------------------------\n" - @printf "Harmony.mk ----------------------------------------\n" - @$(MAKE) -f $(RT_MAKEFILE_DP)/Harmony.mk clean - @printf "target_kernel-module.mk ----------------------------------------\n" - @$(MAKE) -f $(RT_MAKEFILE_DP)/target_kernel-module.mk clean +# +# The project's make settings, for every namespace the project builds with +# make. Harmony.mk includes this file after its own defaults and before the +# namespace's *.mk files, which have the last say. This file holds settings, +# not rules for building: Harmony.mk has those. +# +# Add to a default with +=, replace one with :=, for example: +# +# CFLAGS += -O2 +# LN_FLAGS += -lm +# +# Settings for one namespace go in a .mk file beside its source instead, such +# as authored/.make/.mk. That file is also staged and +# promoted with the namespace's library, so a project built against the +# library reads the same settings. +# +# Harmony.mk's information and clean are double-colon rules, so a recipe +# added here as 'information::' or 'clean::' runs as well as Harmony.mk's. +# Make runs them in the order it read them, so this file's runs first. diff --git a/shared/tool/makefile/Harmony.mk b/shared/tool/makefile/Harmony.mk index 78db1a2..2f95a03 100644 --- a/shared/tool/makefile/Harmony.mk +++ b/shared/tool/makefile/Harmony.mk @@ -1,40 +1,50 @@ # make/Harmony.mk - build *.lib.c and *.CLI.c # written for the Harmony skeleton, always invoked from cwd $REPO_HOME/ -# files have two suffixes by convention, e.g.: X.lib.c or Y.CLI.c +# files have two suffixes by convention, e.g.: X.lib.c or Y.CLI.c +# +# One make reads three layers, each after the one before, so each can change +# what the layers before it set: +# +# 1. Harmony's defaults, in this file +# 2. the project's settings, tool/build_component/makefile +# 3. the namespace's settings, any *.mk beside its source +# +# A layer adds to a default with += and replaces one with :=. The flags the +# build itself depends on (dependency files, the global header, the source +# directory) are added after the layers, so replacing a default cannot remove +# them. +# +# The layers are read before the derived variables and the rules, because +# make expands those as it reads them. A rule a layer defines is read at that +# point too, so its targets and prerequisites cannot use the derived lists. .SUFFIXES: .DELETE_ON_ERROR: +.DEFAULT_GOAL := usage #-------------------------------------------------------------------------------- -# Harmony structure +# 1. Harmony's defaults -SHELL=/bin/bash +SHELL := /bin/bash ECHO := printf "%b\n" -C_SOURCE_DIR ?= authored -C ?= gcc -CFLAGS ?= -std=gnu11 -Wall -Wextra -Wpedantic -finput-charset=UTF-8 -CFLAGS += -MMD -MP -CFLAGS += -include "$(REPO_HOME)/shared/tool/makefile/RT_global.h" -CFLAGS += -I $(C_SOURCE_DIR) -CFLAGS += $(CFLAGS_PROJECT_LOCAL) - -# Project administrators can override this in their local makefile -LIBRARY_NAME ?= $(PROJECT) -LIBRARY_NAME := $(subst -,_,$(LIBRARY_NAME)) +RT_MAKEFILE_DP := $(REPO_HOME)/shared/tool/makefile -BUILD_DIR ?= scratchpad/build -OBJECT_DIR ?= $(BUILD_DIR)/object +# The build component supplies these on the environment, for the namespace +# being built; ?= keeps its values, and a layer can still replace them. +C_SOURCE_DIR ?= authored LIBRARY_DIR ?= scratchpad/made MACHINE_DIR ?= scratchpad/made -LIBRARY_FILE ?= $(LIBRARY_DIR)/lib$(LIBRARY_NAME).a +C := gcc +CFLAGS := -std=gnu11 -Wall -Wextra -Wpedantic -finput-charset=UTF-8 +LN_FLAGS := -L/lib64 -L/lib -# the project library is linked only when the namespace makes one -LN_FLAGS ?= -L$(LIBRARY_DIR) -L/lib64 -L/lib $(if $(object_lib),-l$(LIBRARY_NAME)) -# LN_FLAGS ?= -L$(LIBRARY_DIR) -L/lib64 -L/lib -l$(LIBRARY_NAME) -LN_FLAGS += $(LN_FLAGS_PROJECT_LOCAL) +LIBRARY_NAME := $(PROJECT) + +BUILD_DIR := scratchpad/build +OBJECT_DIR = $(BUILD_DIR)/object KMOD_SOURCE_DIR ?= authored KMOD_CCFLAGS ?= -I $(KMOD_SOURCE_DIR) @@ -42,9 +52,22 @@ KMOD_CCFLAGS ?= -I $(KMOD_SOURCE_DIR) KMOD_CCFLAGS += -include $(REPO_HOME)/shared/tool/makefile/RT_global.h KMOD_OUTPUT_DIR ?= scratchpad/kmod +#-------------------------------------------------------------------------------- +# 2. the project's settings + +include tool/build_component/makefile + +#-------------------------------------------------------------------------------- +# 3. the namespace's settings + +-include $(wildcard $(C_SOURCE_DIR)/*.mk) + #-------------------------------------------------------------------------------- # derived variables +LIBRARY_NAME := $(subst -,_,$(LIBRARY_NAME)) +LIBRARY_FILE := $(LIBRARY_DIR)/lib$(LIBRARY_NAME).a + # source discovery (single dir) c_source_lib := $(wildcard $(C_SOURCE_DIR)/*.lib.c) c_source_exec := $(wildcard $(C_SOURCE_DIR)/*.CLI.c) @@ -60,6 +83,20 @@ object_exec := $(patsubst %, $(OBJECT_DIR)/%.CLI.o, $(c_base_exec)) # executables are made from exec_ sources exec_ := $(patsubst %, $(MACHINE_DIR)/%, $(c_base_exec)) +# A library's product is its archive, its interface and its settings: in the +# single-file C module the .lib.c is its own header, and a .mk holds what a +# program built against the library needs. Both are staged beside the +# archive, so promotion hands a consumer all three. A namespace that makes no +# library stages neither. +interface_ := $(if $(c_source_lib),$(patsubst $(C_SOURCE_DIR)/%,$(LIBRARY_DIR)/%,$(c_source_lib) $(wildcard $(C_SOURCE_DIR)/*.mk))) + +# the flags the build depends on, after the layers +CFLAGS += -MMD -MP +CFLAGS += -include "$(REPO_HOME)/shared/tool/makefile/RT_global.h" +CFLAGS += -I $(C_SOURCE_DIR) +# the project library is linked only when the namespace makes one +LN_FLAGS += -L$(LIBRARY_DIR) $(if $(object_lib),-l$(LIBRARY_NAME)) + #-------------------------------------------------------------------------------- # pull in dependencies @@ -69,25 +106,31 @@ exec_ := $(patsubst %, $(MACHINE_DIR)/%, $(c_base_exec)) #-------------------------------------------------------------------------------- # targets -# when no target is given make uses the first target, this one .PHONY: usage usage: - @echo example usage: make clean - @echo example usage: make library - @echo example usage: make CLI - @echo example usage: make library CLI + @printf "Usage: make [usage|version|information|all|library|lib|CLI|kmod|clean]\n" .PHONY: version version: - @echo makefile version 8.0 + @echo Harmony.mk version 9.0 if [ ! -z "$(C)" ]; then $(C) -v; fi /bin/make -v + @$(MAKE) --no-print-directory -f $(RT_MAKEFILE_DP)/target_kernel-module.mk version +# information and clean are double-colon rules, so the project's settings or a +# namespace's .mk can add a recipe of its own; each recipe runs. .PHONY: information -information: +information:: @printf "· → Unicode middle dot - visible: [%b]\n" "·" + @echo "REPO_HOME: " $(REPO_HOME) + @echo "NAMESPACE: " $(NAMESPACE) @echo "C_SOURCE_DIR: " $(C_SOURCE_DIR) @echo "BUILD_DIR: " $(BUILD_DIR) + @echo "LIBRARY_DIR: " $(LIBRARY_DIR) + @echo "MACHINE_DIR: " $(MACHINE_DIR) + @echo "makefiles read: " $(MAKEFILE_LIST) + @echo "CFLAGS: " $(CFLAGS) + @echo "LN_FLAGS: " $(LN_FLAGS) @echo "c_source_lib: " $(c_source_lib) @echo "c_source_exec: " $(c_source_exec) @echo "c_base_lib: " $(c_base_lib) @@ -95,40 +138,50 @@ information: @echo "object_lib: " $(object_lib) @echo "object_exec: " $(object_exec) @echo "exec_: " $(exec_) + @echo "interface_: " $(interface_) + @$(MAKE) --no-print-directory -f $(RT_MAKEFILE_DP)/target_kernel-module.mk information + +.PHONY: all +all: library CLI kmod -.PHONY: library -library: $(LIBRARY_FILE) +.PHONY: library lib +library lib: $(LIBRARY_FILE) $(interface_) $(LIBRARY_FILE): $(object_lib) - @mkdir -p $(MACHINE_DIR) + @mkdir -p $(LIBRARY_DIR) @if [ -s "$@" ] || [ -n "$(object_lib)" ]; then \ echo "ar rcs $@ $^"; \ ar rcs $@ $^; \ else \ rm -f "$@"; \ - fi + fi -#.PHONY: CLI -#CLI: $(LIBRARY_FILE) $(exec_) +# each staged interface file depends on its source file of the same name +$(interface_): $(LIBRARY_DIR)/%: $(C_SOURCE_DIR)/% + @mkdir -p $(LIBRARY_DIR) + cp $< $@ .PHONY: CLI CLI: library $(exec_) +# The kernel modules are built by a make of their own, which drives Kbuild. +# It defines targets of the same names as this file, so it is run, not +# included. +.PHONY: kmod +kmod: + @$(MAKE) --no-print-directory -f $(RT_MAKEFILE_DP)/target_kernel-module.mk kmod # generally better to use the project local clean scripts, but this will make it so that the make targets can be run again - .PHONY: clean -clean: - rm -f $(LIBRARY_FILE) +clean:: + rm -f $(LIBRARY_FILE) $(interface_) for obj in $(object_lib) $(object_exec); do rm -f $$obj $${obj%.o}.d || true; done for i in $(exec_); do [ -e $$i ] && rm $$i || true; done rm -rf $(BUILD_DIR) + @$(MAKE) --no-print-directory -f $(RT_MAKEFILE_DP)/target_kernel-module.mk clean # recipes -#$(OBJECT_DIR)/%.o: $(C_SOURCE_DIR)/%.c -# @mkdir -p $(OBJECT_DIR) -# $(C) $(CFLAGS) $(CPPFLAGS) -o $@ -c $< $(OBJECT_DIR)/%.o: $(C_SOURCE_DIR)/%.c @mkdir -p $(OBJECT_DIR) -- 2.20.1