Posts tagged programming
.NET creates the so-called intermediate objects while building .NET projects, those are located in the “bin” and “obj” directories. The default is not very satisfying, primarily because if a program from a different machine or a container modifies those, then any cached file system paths that are encoded in the objects will be broken. But also it is probably mostly a legacy behavior to have them split between “bin” and “obj” directories.
I prefer for them to say in one - ".cache", because that’s that they are - cache. With the following configuration objects will be stored inside the ".cache" directory. Furthermore, the objects produced by the native machine in the “native” subdirectory and the ones produced by container software in “container” subdirectory.
|
<PropertyGroup>
<CachePath>.\.cache\native</CachePath>
<CachePath Condition="'$(DOTNET_RUNNING_IN_CONTAINER)' == 'true'">.\.cache\container</CachePath>
<MSBUildProjectExtensionsPath>$(CachePath)\obj\</MSBUildProjectExtensionsPath>
<BaseIntermediateOutputPath>$(CachePath)\obj\</BaseIntermediateOutputPath>
<BaseOutputPath>$(CachePath)\bin\</BaseOutputPath>
</PropertyGroup>
|
If anybody want to go hardcore and cache the intermediate objects based on the RID or architecture triplet, then this can also be done, for example, by adding environment variables to the path.
This is a tongue-in-cheek “draft” for Common Project Layout, version 0. It will probably never become any sort of adopted standard but I think it is good to share some ideas that I had while working on this.
Definition
Common Project Layout (CPL) is a set of good practices for structuring medium-to-large monorepo-like software repositories.
Benefits
CPL helps with code organization. It can be a good “framework” (in a very loose meaning of this word) to modularize product components.
It can make large repositories easier to work with and understand.
Upfront limitations
CPL is strictly designed for “hosting” software and all the non-code assets are left up to the engineers to decide their location.
For example branding assets could be put into the Branding
top-level directory, but on the other hand are we sure they will stay the same with major version?
Since we can agree that we consider documentation “producers” (not the produced artifacts) to be code we could also acknowledge that some assets could have their own versioned subproject.
Requirements
Versioning
CPL requires that the software is versioned inside directories whose names include the version. Recommended pattern is to name directories vMAJOR
where MAJOR
is either the current tagged major version or one that will be if no tags exist. It is also recommended to group the vMAJOR
directories under one common directory, for example Source
.
Subprojects
The vMAJOR
could theoretically contain all the source code mixed together but it should be grouped and organized by their purpose.
Subproject is defined as a directory inside a versioned (vMAJOR
) directory. “Versioned subproject” and “subproject” are synonymous to CPL.
To mark the purpose of a subproject, whether it is to be used as a helper or as a “container” for source that is actually exposed (or binaries created from it), it should be adequately named.
For helpers name does not matter but for source subproject it should be prefixed by project name.
For example we could have this layout:
|
Source/
└── v1/
├── Makefile
├── VERSION
├── admin/
├── make/
├── my-project-app/
└── my-project-util/
|
In the above example my-project-app
and my-project-lib
are the source subproject and admin and make are subproject that are there only to help in building, managing and deploying the actual source subprojects.
At the and it is up to the engineer to choose if something is considered a source subproject. For example: If we have a helper subproject that all it does is hold Docker / Podman files for creating a development container what should we name it? As of now I had named them PROJECT-dev-container
.
Recommendations
Make and admin
I think it is a good practice for each vMAJOR
to have a Makefile, or equivalent in other build system, that will call scripts inside vMAJOR/admin
directory that each take care of some small / specific task.
For example the vMAJOR/Makefile
recipe for build
can call admin/build_my_project_app.py
and admin/build_my_project_lib.py
. Each those scripts would call the “real” build system specific to the subproject they act upon.
VERSION file
It is nice to have a VERSION
file in the vMAJOR
directory. It can be reused by build tools and also to show what was the last version worked upon inside vMAJOR
, the latest git tag can either be put on different major version or simply not be there yet.
References
See those repositories for referencing the CPL layout:
Good practices
Use sh
If you do not need bash
features, then use sh
, it is installed on every UNIX-like system.
Exit on failure
Shell scripts continue even if a commend returns error. To fail right away use:
Trap C-c
Catch Control-c and exit.
Use script directory
Assume we are executing a script from directory /Admin
, where /
is the root of a given project directory.
|
script_path="${0}"
script_root="$(dirname "${script_path}")"
|
We can use ${script_root}
to call other scripts form the Admin
directory, but we can also use it to relate to the /
.
|
project_root="$(realpath "${script_root}/../")"
echo "[INFO] Entering directory: ${project_root}"
cd "${project_root}"
|
So with above we can run commands form /
(repository root). Like for example make
and other:
|
make
python3 ./Admin/serve_1.py
|
Even better
Use Python for repository maintenance scripts.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 |
from os import chdir
from os import path
from subprocess import run
from sys import argv
script_path = path.realpath(__file__)
script_root = path.dirname(script_path)
project_root = path.realpath(path.join(script_root, ".."))
print(f" * Entering directory: {project_root}")
chdir(project_root)
leftover_args = argv[1::]
command_arguments = ["make"] + leftover_args
cmd_string = " ".join(command_arguments)
print(f" * Executing command: {cmd_string}")
run(command_arguments, check=True)
|
Constructing debugging syntax
I wanted to echo parameter values when I set them in my blog’s frog.rkt
config file.
Nothing simpler in Racket!
First I create this macro for echoing a single parameter value when it is set:
1
2
3
4
5
6
7
8
9
10
11
12 |
(define-syntax-rule (verbose-set-parameter parameter-id parameter-value)
(begin
;; Set the parameter.
(parameter-id parameter-value)
;; Then call the parameter and print is's value.
;; The "'parameter-id" is special syntax
;; for turning a "parameter-id" identifier to a symbol.
;; We can also write it like:
;; > (quote parameter-id)
;; to be less confusing.
(printf "[DEBUG] (~a ~v)\n" 'parameter-id (parameter-id))))
|
then, I create a wrapper for above macro that can take multiple parameter pairs:
|
(define-syntax-rule (verbose-set-parameters (parameter-id parameter-value) ...)
(begin
;; Unpack a chain of "(parameter-id parameter-value)" pairs
;; using the "..." syntax.
(verbose-set-parameter parameter-id parameter-value) ...))
|
Using the macro
Afterwards we can call it like so:
|
(verbose-set-parameters
(current-title "XGQT's blog")
(current-author "Maciej Barć"))
|
Notice that even the form of setting a parameter, that is (parameter-procedure "value")
, remains the same, but in reality it is just similar to how the syntax macro pattern-matches on it.
Inspecting macro expansion
In racket-mode
inside GNU Emacs we can inspect the macro expansion with racket-expand-region
. Stepping through the expansion provided this result:
|
(begin
(begin
(current-title "XGQT's blog")
(printf "[DEBUG] (~a ~v)\n" 'current-title (current-title)))
(begin
(current-author "Maciej Barć")
(printf "[DEBUG] (~a ~v)\n" 'current-author (current-author))))
|