This commit is contained in:
2026-09-20 05:27:41 +02:00
parent 831308b2ad
commit 77f228df11
38 changed files with 465 additions and 115 deletions
+1
View File
@@ -5,3 +5,4 @@ build/
*.o
*.a
*.so
__pycache__/
+4 -2
View File
@@ -1,7 +1,9 @@
mainmenu "Zinnia Kernel Configuration"
source "dev/Kconfig"
config KERNEL_FLAVOR
string "Kernel flavor name"
default "vanilla"
source "arch/$(ARCH)/Kconfig"
source "core/Kconfig"
source "dev/Kconfig"
+16
View File
@@ -0,0 +1,16 @@
menu "x86_64 Architecture options"
config X86_64
bool
default y
select ACPI
select HAVE_MMU
select HAVE_MMU_GENERIC
help
64-bit x86 CPU support for AMD and Intel CPUs.
config TSC
bool "Invariant Timestamp Counter support"
default y
endmenu
-1
View File
@@ -1,7 +1,6 @@
kernel_c_args += [
'-mgeneral-regs-only',
'-mno-red-zone',
'-mcmodel=kernel',
'-masm=intel',
]
+3
View File
@@ -1,5 +1,6 @@
#include <zinnia/percpu.h>
#include <zinnia/print.h>
#include <config.h>
#include <stddef.h>
#include <x86_64/apic.h>
#include <x86_64/asm.h>
@@ -119,7 +120,9 @@ static void setup_cpu(struct percpu* cpu) {
void arch_percpu_init() {
hpet_init();
#if CONFIG_TSC
tsc_init();
#endif
// Init the BSP.
setup_cpu(&percpu_bsp);
+3 -3
View File
@@ -94,9 +94,9 @@ void task_entry_thunk() {
asm volatile(
"mov rdi, rbx\n"
"mov rsi, r12\n"
"jmp %c0"
"jmp %0"
:
: "i"(task_entry)
: "r"(task_entry)
: "memory"
);
}
@@ -173,7 +173,7 @@ bool arch_sched_preempt_enable() {
void arch_sched_jump_to_context(struct arch_context* context) {
asm volatile(
"mov rsp, rdi\n"
"jmp %c0\n" ::"i"(interrupt_return)
"jmp %0\n" ::"r"(interrupt_return)
);
}
+2 -2
View File
@@ -11,9 +11,9 @@
[[__init, __naked, __used]]
void _start() {
asm volatile(
"lea rsp, [rip + %0]\n"
"mov rsp, %0\n"
"jmp %1"
:
: "i"(__ld_stack_top), "r"(kernel_entry)
: "r"(__ld_stack_top), "r"(kernel_entry)
);
}
+2 -2
View File
@@ -39,7 +39,7 @@ void arch_syscall_stub() {
" push r15\n"
" xor rbp, rbp\n"
" mov rdi, rsp\n" // SysV ABI first argument.
" call %c4\n" // Call syscall handler.
" call %4\n" // Call syscall handler.
" cli\n"
" pop r15\n"
" pop r14\n"
@@ -66,6 +66,6 @@ void arch_syscall_stub() {
"i"(offsetof(struct percpu, kernel_stack)), // 1
"i"(offsetof(struct gdt, user_code64) | CPL_USER), // 2
"i"(offsetof(struct gdt, user_data) | CPL_USER), // 3
"i"(syscall_dispatch) // 4
"r"(syscall_dispatch) // 4
);
}
+52 -53
View File
@@ -1,6 +1,14 @@
#include <zinnia/compiler.h>
#include <arch/usercopy.h>
void arch_usercopy_allow(bool state) {
if (state) {
asm volatile("stac" ::: "memory");
} else {
asm volatile("clac" ::: "memory");
}
}
extern void x86_64_read_start();
extern void x86_64_read_end();
extern void x86_64_read_fault();
@@ -11,37 +19,34 @@ static const struct usercopy_region read_region = {
.fault_ip = x86_64_read_fault,
};
[[__naked]]
bool arch_usercopy_read(void* dst, const __user void* src, size_t len, struct usercopy_region** region) {
asm volatile(
"stac\n"
// Setup regs for `rep movsb`.
"xchg rcx, rdx\n"
"mov rax, [rip + %0]\n"
"xchg [rdx], rax\n"
" xchg rcx, rdx\n"
" mov rax, [rip + %0]\n"
" xchg [rdx], rax\n"
".global x86_64_read_start\n"
"x86_64_read_start:\n"
"rep movsb\n"
" x86_64_read_start:\n"
" rep movsb\n"
".global x86_64_read_end\n"
"x86_64_read_end:\n"
"xor rax, rax\n"
"xchg [rdx], rax\n"
"mov rax, 1\n"
"clac\n"
"ret\n"
" xor rax, rax\n"
" xchg [rdx], rax\n"
" mov rax, 1\n"
" ret\n"
".global x86_64_read_fault\n"
"x86_64_read_fault:\n"
"xor rax, rax\n"
"xchg [rdx], rax\n"
"clac\n"
"ret\n"
" xor rax, rax\n"
" xchg [rdx], rax\n"
" ret\n"
:
: "i"(&read_region)
: "memory"
);
return false;
}
extern void x86_64_write_start();
@@ -54,37 +59,34 @@ static const struct usercopy_region write_region = {
.fault_ip = x86_64_write_fault,
};
[[__naked]]
bool arch_usercopy_write(__user void* dst, const void* src, size_t len, struct usercopy_region** region) {
asm volatile(
"stac\n"
// Setup regs for `rep movsb`.
"xchg rcx, rdx\n"
"mov rax, [rip + %0]\n"
"mov [rdx], rax\n"
" xchg rcx, rdx\n"
" mov rax, [rip + %0]\n"
" mov [rdx], rax\n"
".global x86_64_write_start\n"
"x86_64_write_start:\n"
"rep movsb\n"
" x86_64_write_start:\n"
" rep movsb\n"
".global x86_64_write_end\n"
"x86_64_write_end:\n"
"xor rax, rax\n"
"xchg [rdx], rax\n"
"mov rax, 1\n"
"clac\n"
"ret\n"
" x86_64_write_end:\n"
" xor rax, rax\n"
" xchg [rdx], rax\n"
" mov rax, 1\n"
" ret\n"
".global x86_64_write_fault\n"
"x86_64_write_fault:\n"
"xor rax, rax\n"
"xchg [rdx], rax\n"
"clac\n"
"ret\n"
" xor rax, rax\n"
" xchg [rdx], rax\n"
" ret\n"
:
: "i"(&write_region)
: "memory"
);
return false;
}
extern void x86_64_strlen_start();
@@ -100,36 +102,33 @@ static const struct usercopy_region strlen_region = {
[[__naked]]
bool arch_usercopy_strlen(const __user char* str, size_t max, size_t* len, struct usercopy_region** region) {
asm volatile(
"stac\n"
"mov rax, [rip + %0]\n"
"xchg [rcx], rax\n"
" mov rax, [rip + %0]\n"
" xchg [rcx], rax\n"
".global x86_64_strlen_start\n"
"x86_64_strlen_start:\n"
"xor r8, r8\n"
" x86_64_strlen_start:\n"
" xor r8, r8\n"
".Lloop:\n"
"cmp byte ptr [rdi + r8], 0\n"
"je .Lleave\n"
"inc r8\n"
"cmp rsi, r8\n"
"jne .Lloop\n"
" cmp byte ptr [rdi + r8], 0\n"
" je .Lleave\n"
" inc r8\n"
" cmp rsi, r8\n"
" jne .Lloop\n"
".Lleave:\n"
"mov [rdx], r8\n"
" mov [rdx], r8\n"
".global x86_64_strlen_end\n"
"x86_64_strlen_end:\n"
"xor rax, rax\n"
"xchg [rcx], rax\n"
"mov rax, 1\n"
"clac\n"
"ret\n"
" xor rax, rax\n"
" xchg [rcx], rax\n"
" mov rax, 1\n"
" ret\n"
".global x86_64_strlen_fault\n"
"x86_64_strlen_fault:\n"
"xor rax, rax\n"
"xchg [rcx], rax\n"
"clac\n"
"ret\n"
" xor rax, rax\n"
" xchg [rcx], rax\n"
" ret\n"
:
: "i"(&strlen_region)
: "memory"
+8 -6
View File
@@ -54,27 +54,29 @@ SECTIONS
.dynsym : {
__ld_dynsym_start = .;
*(.dynsym)
KEEP(*(.dynsym*))
__ld_dynsym_end = .;
} :data
.dynstr : {
__ld_dynstr_start = .;
*(.dynstr)
KEEP(*(.dynstr*))
__ld_dynstr_end = .;
} :data
.dynamic : {
*(.dynamic)
*(.dynamic*)
} :data :dynamic
.bss : {
.bss ALIGN(CONSTANT(MAXPAGESIZE)) (NOLOAD) : {
*(.*bss .*bss.*)
*(COMMON)
/* Small stack to bootstrap the allocator */
. = ALIGN(16);
__ld_stack_bottom = .;
. += 0x2000;
__ld_stack_top = .;
*(.*bss .*bss.*)
*(COMMON)
} :data
/* Per-CPU data block. MUST always come last. */
+18
View File
@@ -0,0 +1,18 @@
menu "Kernel Core options"
source "core/mem/Kconfig"
choice SCHED_ALG
prompt "Scheduler algorithm"
default SCHED_DUMB
help
The algorithm to use for scheduling.
config SCHED_DUMB
bool "Dumb Scheduler"
help
Very basic scheduler, without any priorities.
endchoice
endmenu
+8
View File
@@ -0,0 +1,8 @@
menu "Memory Management options"
config HAVE_MMU
bool
config HAVE_MMU_GENERIC
bool
endmenu
+6 -1
View File
@@ -1,7 +1,6 @@
kernel_sources += files(
'fault.c',
'init.c',
'pmap.c',
'pmm.c',
'slab.c',
'usercopy.c',
@@ -9,3 +8,9 @@ kernel_sources += files(
'vma.c',
'vmo.c',
)
if kconfig.get('CONFIG_HAVE_MMU', 'n') == 'y'
kernel_sources += files(
'pmap.c',
)
endif
+4
View File
@@ -3,8 +3,12 @@
#include <zinnia/pmap.h>
#include <zinnia/spin.h>
#include <zinnia/vma.h>
#include <config.h>
#include <string.h>
// TODO: For now, we assume that we can do generic PTE lookups on every arch that has a MMU.
static_assert(CONFIG_HAVE_MMU_GENERIC);
errno_t pmap_new_kernel(struct pmap* pt, enum alloc_flags flags) {
flags &= ~ALLOC_NOZERO;
+12 -3
View File
@@ -3,13 +3,22 @@
#include <arch/usercopy.h>
bool usercopy_read(void* dst, const __user void* src, size_t len) {
return arch_usercopy_read(dst, src, len, &percpu_get()->sched.current->usercopy_region);
arch_usercopy_allow(true);
bool success = arch_usercopy_read(dst, src, len, &percpu_get()->sched.current->usercopy_region);
arch_usercopy_allow(false);
return success;
}
bool usercopy_write(__user void* dst, const void* src, size_t len) {
return arch_usercopy_write(dst, src, len, &percpu_get()->sched.current->usercopy_region);
arch_usercopy_allow(true);
bool success = arch_usercopy_write(dst, src, len, &percpu_get()->sched.current->usercopy_region);
arch_usercopy_allow(false);
return success;
}
bool usercopy_strlen(const __user char* str, size_t max, size_t* len) {
return arch_usercopy_strlen(str, max, len, &percpu_get()->sched.current->usercopy_region);
arch_usercopy_allow(true);
bool success = arch_usercopy_strlen(str, max, len, &percpu_get()->sched.current->usercopy_region);
arch_usercopy_allow(false);
return success;
}
+4 -1
View File
@@ -1,4 +1,7 @@
kernel_sources += files(
'core.c',
'sched_dumb.c',
)
if kconfig.get('CONFIG_SCHED_DUMB', 'n') == 'y'
kernel_sources += files('sched_dumb.c')
endif
+3
View File
@@ -12,4 +12,7 @@ config ACPI
help
Adds support for ACPI firmware, tables and devices.
source "dev/block/Kconfig"
source "dev/pci/Kconfig"
endmenu
+5
View File
@@ -0,0 +1,5 @@
menu "Block device drivers"
source "dev/block/nvme/Kconfig"
endmenu
+3
View File
@@ -0,0 +1,3 @@
if kconfig.get('CONFIG_NVME_CORE', 'n') != 'n'
subdir('nvme')
endif
+13
View File
@@ -0,0 +1,13 @@
menu "NVMe Support"
config NVME_CORE
bool
default y
config NVME_PCI
bool "NVMe via PCI transport"
default y
depends on PCI
depends on NVME_CORE
endmenu
+12
View File
@@ -0,0 +1,12 @@
#include <zinnia/cmdline.h>
#include <zinnia/module.h>
#include <zinnia/print.h>
void nvme_init(struct module* mod) {
log("Hello world from NVMe!\n");
}
void do_nvme(bool on) {}
CMDLINE_OPTION("nvme_on", "My description", do_nvme);
MODULE("NVME", "Marvin Friedrich", "ISC", "1.0", nvme_init);
+12
View File
@@ -0,0 +1,12 @@
nvme_sources = files('core.c')
if kconfig.get('CONFIG_NVME_PCI', 'n') == 'y'
nvme_sources += files('pci.c')
endif
modules += {
'nvme': {
'modular': true, #kconfig.get('CONFIG_NVME_CORE') == 'm',
'sources': nvme_sources,
},
}
View File
+1
View File
@@ -0,0 +1 @@
#pragma once
+5 -2
View File
@@ -1,9 +1,12 @@
subdir('common')
subdir('block')
if cfg.get('CONFIG_ACPI', 0) == 1
if kconfig.get('CONFIG_ACPI', 'n') == 'y'
subdir('acpi')
endif
if cfg.get('CONFIG_DT', 0) == 1
if kconfig.get('CONFIG_DT', 'n') == 'y'
subdir('dt')
endif
module_lds = meson.current_source_dir() / 'module.ld'
+101
View File
@@ -0,0 +1,101 @@
/* Linker script for kernel modules */
ENTRY(__module_start)
PHDRS
{
text PT_LOAD FLAGS(5);
rodata PT_LOAD FLAGS(4);
data PT_LOAD FLAGS(6);
dynamic PT_DYNAMIC FLAGS(6);
modversion 0x60000001;
modauthor 0x60000002;
moddesc 0x60000003;
modlicense 0x60000004;
modcmdline 0x60000005;
}
SECTIONS
{
. = ALIGN(CONSTANT(MAXPAGESIZE));
/* Code */
.text : {
*(.init .init.*)
*(.text .text.*)
*(.fini .fini.*)
*(.plt .plt.*)
} :text
. = ALIGN(CONSTANT(MAXPAGESIZE));
/* Read-only data */
.rodata : {
*(.rodata .rodata.*)
*(.hash .hash.*)
*(.got .got.*)
*(.got.plt .got.plt.*)
} :rodata
.cmdline : {
KEEP(*(.cmdline))
} :rodata :modcmdline
.mod.version : {
KEEP(*(.mod.version))
} :rodata :modversion
.mod.author : {
KEEP(*(.mod.author))
} :rodata :modauthor
.mod.desc : {
KEEP(*(.mod.desc))
} :rodata :moddesc
.mod.license : {
KEEP(*(.mod.license))
} :rodata :modlicense
. = ALIGN(CONSTANT(MAXPAGESIZE));
/* Read-Write data */
.data : {
*(.data)
*(.data.*)
} :data
/* Zero-initialized memory */
.bss : {
*(.*bss .*bss.*)
*(COMMON)
} :data
.dynsym : {
*(.dynsym .dynsym.*)
} :data
.dynstr : {
*(.dynstr .dynstr.*)
} :data
.rela.dyn : {
*(.rela.dyn .rela.dyn.*)
} :data
/* Dynamic tags */
.dynamic : {
*(.dynamic)
} :data :dynamic
/DISCARD/ :
{
*(.comment*)
*(.eh_frame*)
*(.rel.eh_frame*)
*(.relr.dyn .relr.dyn.*)
*(.gcc_except_table*)
*(.note*)
*(.interp)
}
}
+7
View File
@@ -0,0 +1,7 @@
menu "PCI bus drivers"
config PCI
bool "PCI core drivers"
default y
endmenu
-1
View File
@@ -2,6 +2,5 @@
#include <zinnia/errno.h>
#include <stdint.h>
#include <uapi/archctl.h>
errno_t arch_archctl(uintptr_t op, uintptr_t value);
+1 -1
View File
@@ -1,3 +1,3 @@
#pragma once
#include <zinnia/elf.h>
// TODO: Ensure bits are defining values right.
+2 -13
View File
@@ -57,10 +57,10 @@ struct cmdline_option {
};
union {
const char** str_limit;
struct cmdline_option_limit_int {
struct cmdline_limit_int {
intptr_t min, max;
}* intptr_limit;
struct cmdline_option_limit_uint {
struct cmdline_limit_uint {
uintptr_t min, max;
}* uintptr_limit;
void* limit;
@@ -69,14 +69,3 @@ struct cmdline_option {
// Parses the command line and invokes all options.
void cmdline_parse(const char* cmdline);
static_assert(
0 ==
_Generic(
(cmdline_parse),
void (*)(const char*): CMDLINE_STRING,
void (*)(bool): CMDLINE_BOOL,
void (*)(intptr_t): CMDLINE_INTPTR,
void (*)(uintptr_t): CMDLINE_UINTPTR
)
);
+1
View File
@@ -15,6 +15,7 @@
#define __likely(x) __builtin_expect(!!(x), 1)
#define __unlikely(x) __builtin_expect(!!(x), 0)
#define __always_inline gnu::always_inline
#define __visibility gnu::visibility
// Only clang has these hints, GCC will throw a warning here.
// They also don't support the C23 attribute format.
+7
View File
@@ -0,0 +1,7 @@
#pragma once
#include <zinnia/module.h>
struct device {
const char* name;
struct module* owner;
};
+4 -2
View File
@@ -1,10 +1,10 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <zinnia/compiler.h>
#include <zinnia/types.h>
#include <zinnia/utils.h>
#include <stddef.h>
#include <stdint.h>
#define __init __used, __section(".init.text"), __cold
#define __initdata __used, __section(".init.data")
@@ -28,7 +28,9 @@ struct boot_info {
phys_t rsdp;
};
[[__visibility("hidden")]]
extern uint8_t __ld_stack_top[];
[[__visibility("hidden")]]
extern uint8_t __ld_stack_bottom[];
extern phys_t rsdp_addr;
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <zinnia/compiler.h>
#include <zinnia/utils.h>
#include <stdint.h>
#define MODULE_PROPERTY(name, data) \
[[__used, __section(".mod." name)]] \
static const char UNIQUE_IDENT(_name)[] = data;
#define MODULE(desc, author, license, version, entry) \
void __module_start(struct module* _module) { \
entry(_module); \
} \
MODULE_PROPERTY("author", author); \
MODULE_PROPERTY("desc", desc); \
MODULE_PROPERTY("version", version); \
MODULE_PROPERTY("license", license);
struct module {
const char* name;
struct module* owner;
};
#define THIS_MODULE (struct module*)nullptr
+19 -1
View File
@@ -9,7 +9,25 @@
ASSERT_TYPE(struct arch_percpu); // bits/percpu.h
#define __percpu __used, __section(".percpu")
/*
On using per-CPU data
---------------------
Defining variables with `[[__percpu]]` will put them in a special section.
This section is unique for every CPU. To access the correct memory of the
caller's CPU, use `percpu_ref`.
# Example
[[__percpu]]
int my_cpu_data = 0;
int get_foo() {
return *percpu_ref(my_cpu_data);
}
*/
#define __percpu __used, __section(".percpu")
#define percpu_ref(x) ((typeof(&(x)))((void*)percpu_get() + ((void*)&(x) - (void*)&percpu_bsp)))
// CPU-relative information.
struct percpu {
+1 -1
View File
@@ -19,7 +19,7 @@ errno_t vma_new(struct vma** out);
void vma_free(struct vma* v);
errno_t vma_map(
struct vma* vm,
struct vma* vma,
struct vmo* vmo,
uintptr_t addr,
size_t len,
+64 -20
View File
@@ -38,57 +38,58 @@ add_project_arguments(
'-fno-stack-protector',
'-ffunction-sections',
'-fdata-sections',
'-ffreestanding',
'-nostdinc', # We provide our own headers.
'-fno-stack-protector',
language: 'c',
)
add_project_link_arguments(
'-ffreestanding',
'-nostdlib',
'-znoexecstack',
'-Wl,--gc-sections',
'-Wl,--build-id=none',
language: 'c',
)
common_includes = include_directories('include')
common_includes = [
include_directories('include'),
include_directories('arch' / target_machine.cpu_family() / 'include'),
]
cfg = configuration_data()
cfg.set_quoted('ZINNIA_VERSION', meson.project_version())
cfg.set_quoted('ZINNIA_ARCH', target_machine.cpu_family())
cfg.set_quoted('ZINNIA_COMPILER_ID', c_compiler.get_id() + ' ' + c_compiler.version())
cfg.set_quoted('ZINNIA_LINKER_ID', c_compiler.get_linker_id())
#
# Parse kernel config
kv = import('keyval')
kconfig = kv.load(get_option('config_path'))
foreach k, v : kconfig
if v == 'y'
cfg.set10(k, true)
elif v == 'n'
cfg.set10(k, false)
else
cfg.set(k, v)
endif
endforeach
kernel_c_args = [
'-ffreestanding',
'-nostdinc', # We provide our own headers.
'-fno-stack-protector',
'-fno-pie',
]
kernel_c_args = []
kernel_lds = meson.current_source_dir() / 'boot' / 'kernel.ld'
kernel_link_args = [
'-ffreestanding',
'-nostdlib',
'-static',
'-lgcc',
'-static-libgcc',
'-T' + kernel_lds,
'-Wl,--export-dynamic',
]
kernel_includes = [
include_directories('arch' / target_machine.cpu_family() / 'include'),
]
kernel_includes = []
kernel_sources = []
modules = {}
# Goes first so we can add arch-specific flags to drivers.
subdir('arch' / target_machine.cpu_family())
@@ -104,18 +105,61 @@ configure_file(
output: 'config.h',
)
mod_summary = {}
foreach name, v : modules
is_modular = v.get('modular', false)
mod_sources = v.get('sources')
mod_c_args = v.get('c_args', [])
mod_link_args = v.get('link_args', [])
mod_includes = v.get('include_directories', [])
mod_link = v.get('link_with', [])
if is_modular
shared_library(
name,
sources: mod_sources,
c_args: ['-fPIC', '-fPIE', '-DIN_MODULE', mod_c_args],
link_args: [
'-T' + module_lds, mod_link_args,
],
link_depends: module_lds,
include_directories: [
common_includes,
kernel_includes, # We want this, actually
mod_includes,
],
name_prefix: '',
name_suffix: 'kso',
link_with: mod_link,
override_options: ['b_lundef=false'],
#link_depends: kernel.full_path(),
)
else
kernel_sources += mod_sources
kernel_c_args += mod_c_args
kernel_includes += mod_includes
kernel_link_args += mod_link_args
endif
mod_summary += {name: is_modular}
endforeach
# Kernel
executable(
'zinnia',
include_directories: [
common_includes,
kernel_includes,
],
c_args: kernel_c_args,
c_args: ['-DIN_KERNEL', kernel_c_args],
link_args: kernel_link_args,
link_depends: kernel_lds,
sources: [
kernel_sources,
],
sources: kernel_sources,
install: true,
install_dir: get_option('datadir') / 'zinnia',
pie: true,
)
# Print a summary
summary(mod_summary, section: 'Driver configuration')
summary(kconfig, bool_yn: true, section: 'Kernel configuration')
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
# Small script to print all options available inside a built kernel.
# fmt: off
import sys
from elftools.elf.elffile import ELFFile
SIZEOF_PTR = 8
SIZEOF_STRUCT = 40
ENDIAN = "little"
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Provide a path to the kernel binary to use")
sys.exit(1)
with open(sys.argv[1], "rb") as elffile:
file = ELFFile(elffile)
version = "?"
author = "?"
desc = "?"
license = "?"
for segment in file.iter_segments():
match segment["p_type"]:
case 0x60000001: version = segment.data()[:-1].decode()
case 0x60000002: author = segment.data()[:-1].decode()
case 0x60000003: desc = segment.data()[:-1].decode()
case 0x60000004: license = segment.data()[:-1].decode()
print(f"Module info for \"{sys.argv[1]}\"")
print(f"Description:\t{desc}")
print(f"Version:\t{version}")
print(f"Author(s):\t{author}")
print(f"License:\t{license}")