Compare commits
2 Commits
035ffe0de1
...
eee25bf37c
| Author | SHA1 | Date | |
|---|---|---|---|
| eee25bf37c | |||
| 472289a641 |
Executable
+300
@@ -0,0 +1,300 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
### TODO: ensure files deleted in the maintainer repository are
|
||||||
|
### also deleted in the students repository
|
||||||
|
|
||||||
|
### WARN: this script will not work unless the remote name is `origin` (default).
|
||||||
|
|
||||||
|
### WARN: this script expects you to connect to github via ssh.
|
||||||
|
### ensure you registered your public key.
|
||||||
|
|
||||||
|
# constant definitions
|
||||||
|
|
||||||
|
# WARN: make sure you understand the implications before
|
||||||
|
# you change the value (or name) of a constant!
|
||||||
|
|
||||||
|
required_tools=(
|
||||||
|
"cat"
|
||||||
|
"dirname"
|
||||||
|
"find"
|
||||||
|
"git"
|
||||||
|
"grep"
|
||||||
|
"pandoc"
|
||||||
|
"realpath"
|
||||||
|
"sed"
|
||||||
|
"tar"
|
||||||
|
"tput"
|
||||||
|
)
|
||||||
|
common_dir="../common"
|
||||||
|
laboratory_dir="./laboratories"
|
||||||
|
student_repo_name="student-repository"
|
||||||
|
student_repo_url="git@github.zhaw.ch:CT/ct1-students.git"
|
||||||
|
code_start="STUDENTS: To be programmed"
|
||||||
|
code_end="END: To be programmed"
|
||||||
|
make_start="solution[s]*=start"
|
||||||
|
make_end="solution[s]*=end"
|
||||||
|
md_solution_start="solution[s]*=start"
|
||||||
|
md_solution_end="solution[s]*=end"
|
||||||
|
md_student_start="student[s]*=start"
|
||||||
|
md_student_end="student[s]*=end"
|
||||||
|
keili_dir_name="keil"
|
||||||
|
archive_name="release.tar"
|
||||||
|
exclude_file_name=".release-ignore"
|
||||||
|
|
||||||
|
# sanity checks for variables
|
||||||
|
if [[ "${laboratory_dir}" =~ ".." ]]; then
|
||||||
|
echo "'laboratory_dir' has to be a child directory of the one containing this script"
|
||||||
|
fi
|
||||||
|
if [[ "." != "$(dirname "${student_repo_name}")" ]]; then
|
||||||
|
echo "'student_repo_name' may not contain directories!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ "." != "$(dirname "${keili_dir_name}")" ]]; then
|
||||||
|
echo "'keili_dir_name' may not contain directories!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [[ "." != "$(dirname "${exclude_file_name}")" ]]; then
|
||||||
|
echo "'exclude_file_name' may not contain directories!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# helper function declarations
|
||||||
|
|
||||||
|
## print highlighted text
|
||||||
|
##
|
||||||
|
## used the same as `echo`
|
||||||
|
function print_highlighted {
|
||||||
|
if ! $(which tput &>/dev/null) || [[ -z "$(tput smso)" ]]; then
|
||||||
|
echo "$*"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
tput smso
|
||||||
|
echo "$*"
|
||||||
|
tput sgr0
|
||||||
|
}
|
||||||
|
|
||||||
|
## print yellow text
|
||||||
|
##
|
||||||
|
## used the same as `echo`
|
||||||
|
function print_warning {
|
||||||
|
if ! $(which tput &>/dev/null); then
|
||||||
|
echo "$*"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
tput setaf 3
|
||||||
|
echo "$*"
|
||||||
|
tput sgr0
|
||||||
|
}
|
||||||
|
|
||||||
|
## print red text
|
||||||
|
##
|
||||||
|
## used the same as `echo`
|
||||||
|
function print_error {
|
||||||
|
if ! $(which tput &>/dev/null); then
|
||||||
|
echo "$*"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
tput setaf 1
|
||||||
|
echo "$*"
|
||||||
|
tput sgr0
|
||||||
|
}
|
||||||
|
|
||||||
|
# main logic
|
||||||
|
|
||||||
|
# ensure the required tools are available
|
||||||
|
unset error_message
|
||||||
|
for tool in "${required_tools}"; do
|
||||||
|
if ! $(which "${tool}" &>/dev/null); then
|
||||||
|
error_message+="${tool}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ -n "${error_message}" ]]; then
|
||||||
|
print_error "missing the following tools: [ ${error_message} ]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ensure commands are executed in the directory where the
|
||||||
|
# script is located
|
||||||
|
root_dir="$(realpath "${BASH_SOURCE}" | dirname -)"
|
||||||
|
cd "${root_dir}"
|
||||||
|
|
||||||
|
# check that there are no paths with spaces
|
||||||
|
illegal_paths="$(find . -type d -name ".git" -prune -o -name "*" -print |
|
||||||
|
sort | grep ' ')"
|
||||||
|
if [[ -n "${illegal_paths}" ]]; then
|
||||||
|
print_error "no spaces in paths allowed!"
|
||||||
|
print_error "the following paths are illegal:"
|
||||||
|
echo ""
|
||||||
|
echo "${illegal_paths}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ensure the working tree of the maintainer repository is not behind
|
||||||
|
# no need to check the students repository, as it is derived from the
|
||||||
|
# maintainer repository
|
||||||
|
git fetch &>/dev/null
|
||||||
|
if [[ -n "$(git diff origin/HEAD)" ]]; then
|
||||||
|
print_warning "working tree of the maintainer repository differs from the remotes head."
|
||||||
|
print_warning "pull and/or commit/push first to resolve the differences."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# remove trailing whitespace and convert to unix file format
|
||||||
|
bash "${common_dir}/trailing-whitespace.sh" -u .
|
||||||
|
|
||||||
|
# ensure the students repository is checked out
|
||||||
|
if [[ ! -d "${student_repo_name}" ]]; then
|
||||||
|
print_highlighted "cloning students repository"
|
||||||
|
git clone "${student_repo_url}" "${student_repo_name}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ensure the students repository is ignored
|
||||||
|
if [[ -z "$(cat "./.gitignore" | grep "${student_repo_name}")" ]]; then
|
||||||
|
echo "${student_repo_name}/" >>"./.gitignore"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# remove solutions from source code
|
||||||
|
code_files=($(
|
||||||
|
find "${laboratory_dir}" \
|
||||||
|
-type f \
|
||||||
|
-name "*.[chs]"
|
||||||
|
))
|
||||||
|
|
||||||
|
for file in "${code_files[@]}"; do
|
||||||
|
sed -i "
|
||||||
|
/${code_start}/,/${code_end}/ {
|
||||||
|
/${code_start}/n
|
||||||
|
/${code_end}/!d
|
||||||
|
}" ${file}
|
||||||
|
|
||||||
|
# add blank lines to show where code goes
|
||||||
|
sed -i "/${code_start}/a\\\\" ${file}
|
||||||
|
sed -i "/${code_start}/a\\\\" ${file}
|
||||||
|
sed -i "/${code_start}/a\\\\" ${file}
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "> removed solutions from source files."
|
||||||
|
|
||||||
|
# removing solutions from makefiles
|
||||||
|
make_files=($(
|
||||||
|
find "${laboratory_dir}" \
|
||||||
|
-type f \
|
||||||
|
-name "Makefile" \
|
||||||
|
-or \
|
||||||
|
-name "*.mk"
|
||||||
|
))
|
||||||
|
|
||||||
|
for file in "${make_files[@]}"; do
|
||||||
|
sed -i "/${make_start}/,/${make_end}/d" ${file}
|
||||||
|
done
|
||||||
|
|
||||||
|
# remove solutions from lab instructions
|
||||||
|
md_files=($(
|
||||||
|
find "${laboratory_dir}" \
|
||||||
|
-type f \
|
||||||
|
-name "*.md"
|
||||||
|
))
|
||||||
|
|
||||||
|
for file in "${md_files[@]}"; do
|
||||||
|
sed -i "/${md_solution_start}/,/${md_solution_end}/d" ${file}
|
||||||
|
sed -i "/${md_student_start}/d" ${file}
|
||||||
|
sed -i "/${md_student_end}/d" ${file}
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "> removed solutions from lab instructions."
|
||||||
|
|
||||||
|
# convert readmes from `pandoc markdown` to `github flavoured markdown` and pdf
|
||||||
|
echo "converting markdown files to pdfs"
|
||||||
|
echo "(this may take a while)"
|
||||||
|
|
||||||
|
template="$(realpath "${common_dir}/template.tex")"
|
||||||
|
unset pdf_files
|
||||||
|
for file in "${md_files[@]}"; do
|
||||||
|
filename="${file##*/}"
|
||||||
|
pushd "${file%/*}" &>/dev/null
|
||||||
|
pandoc \
|
||||||
|
-f markdown \
|
||||||
|
-t pdf \
|
||||||
|
--pdf-engine pdflatex \
|
||||||
|
--listings \
|
||||||
|
--template "${template}" \
|
||||||
|
"${filename}" \
|
||||||
|
-o "${filename/%md/pdf}"
|
||||||
|
pandoc \
|
||||||
|
-f markdown \
|
||||||
|
-t gfm \
|
||||||
|
"${filename}" \
|
||||||
|
-o "${filename}"
|
||||||
|
popd &>/dev/null
|
||||||
|
pdf_files+=("${file/%md/pdf}")
|
||||||
|
done
|
||||||
|
unset template
|
||||||
|
|
||||||
|
echo "> converted readme files to github flavoured markdown."
|
||||||
|
|
||||||
|
# generate zip archives of keili projects
|
||||||
|
keili_dirs=($(find . -type d -name "${keili_dir_name}"))
|
||||||
|
for item in "${keili_dirs[@]}"; do
|
||||||
|
pushd "$(dirname "${item}")" &>/dev/null
|
||||||
|
zip -r "${keili_dir_name}.zip" "${keili_dir_name}" &>/dev/null
|
||||||
|
popd &>/dev/null
|
||||||
|
done
|
||||||
|
unset keili_dirs
|
||||||
|
|
||||||
|
keili_archives=($(find "${laboratory_dir}" -name "${keili_dir_name}.zip"))
|
||||||
|
|
||||||
|
# copy modified contents to students repository
|
||||||
|
exclude_patterns=("${exclude_file_name}")
|
||||||
|
exclude_patterns+=("${keili_dir_name}")
|
||||||
|
exclude_patterns+=("*.pptx")
|
||||||
|
exclude_patterns+=("*.docx")
|
||||||
|
|
||||||
|
# collect exclude globs
|
||||||
|
ignore_files=($(find . -type f -iname "${exclude_file_name}"))
|
||||||
|
for file in "${ignore_files[@]}"; do
|
||||||
|
temp_path="$(dirname ${file})/"
|
||||||
|
temp_patterns=($(cat "${file}"))
|
||||||
|
exclude_patterns+=(${temp_patterns[@]/#/${temp_path}})
|
||||||
|
done
|
||||||
|
|
||||||
|
# remove contents of the student repository to avoid
|
||||||
|
# orphaned files/directories
|
||||||
|
find "${student_repo_name}" \
|
||||||
|
-mindepth 1 \
|
||||||
|
-maxdepth 1 \
|
||||||
|
-not -name ".git" \
|
||||||
|
-exec rm -rf {} \;
|
||||||
|
|
||||||
|
# create release using `tar`
|
||||||
|
tar -cf "${archive_name}" \
|
||||||
|
-h \
|
||||||
|
--exclude-vcs \
|
||||||
|
--exclude-vcs-ignore \
|
||||||
|
--exclude="${archive_name}" \
|
||||||
|
"${exclude_patterns[@]/#/--exclude=}" \
|
||||||
|
"${keili_archives[@]/#/--add-file=}" \
|
||||||
|
"${pdf_files[@]/#/--add-file=}" \
|
||||||
|
.
|
||||||
|
tar -xf "${archive_name}" \
|
||||||
|
-C "${student_repo_name}/"
|
||||||
|
|
||||||
|
# reset repository after release creation
|
||||||
|
git reset --hard &>/dev/null
|
||||||
|
git clean -fd &>/dev/null
|
||||||
|
rm "${keili_archives[@]}" "${pdf_files[@]}"
|
||||||
|
|
||||||
|
# print message to report success and a reminder to commit and
|
||||||
|
# push the students repository
|
||||||
|
relative_path="$(dirname "${BASH_SOURCE}")/${student_repo_name}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "successfully prepared the release for the students repository."
|
||||||
|
echo "generated release is found at \`${relative_path}\`."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
print_highlighted "remember to commit and push the generated release!"
|
||||||
|
|
||||||
|
echo ""
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
SHELL := /usr/bin/env bash
|
||||||
|
|
||||||
|
SRC_DIR := ./src
|
||||||
|
INC_DIR := ./inc
|
||||||
|
OBJ_DIR := ./obj
|
||||||
|
BIN_DIR := ./bin
|
||||||
|
LIB_DIR := ../../libctboard
|
||||||
|
COMMON_DIR := ../../common
|
||||||
|
|
||||||
|
AS_SRC := table.s
|
||||||
|
|
||||||
|
C_DEF := PLATFORM_M0
|
||||||
|
C_INC :=
|
||||||
|
C_SRC :=
|
||||||
|
|
||||||
|
include $(COMMON_DIR)/root.mk
|
||||||
|
include $(COMMON_DIR)/pdf.mk
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
SHELL := /usr/bin/env bash
|
||||||
|
|
||||||
|
MD_FILES += readme.md
|
||||||
|
|
||||||
|
PDF_FILES := $(MD_FILES:.md=.pdf)
|
||||||
|
|
||||||
|
VERBOSITY :=
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: all clean clean_all test pdf repdf remove_pdf debug_pdf gfm
|
||||||
|
|
||||||
|
|
||||||
|
clean_all:: remove_pdf
|
||||||
|
|
||||||
|
|
||||||
|
pdf: $(PDF_FILES)
|
||||||
|
|
||||||
|
|
||||||
|
repdf: remove_pdf pdf
|
||||||
|
|
||||||
|
|
||||||
|
remove_pdf:
|
||||||
|
@rm -f $(PDF_FILES) || true
|
||||||
|
|
||||||
|
|
||||||
|
debug_pdf: VERBOSITY := --verbose
|
||||||
|
debug_pdf: $(PDF_FILES)
|
||||||
|
|
||||||
|
|
||||||
|
%.pdf: %.md
|
||||||
|
pandoc \
|
||||||
|
--pdf-engine pdflatex \
|
||||||
|
--listings \
|
||||||
|
--template $(COMMON_DIR)/template.tex \
|
||||||
|
-f markdown \
|
||||||
|
-t pdf \
|
||||||
|
$(VERBOSITY) \
|
||||||
|
-o $@ \
|
||||||
|
$<
|
||||||
|
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
|
||||||
|
SHELL := /usr/bin/env bash
|
||||||
|
|
||||||
|
PREFIX := arm-none-eabi-
|
||||||
|
CC := $(PREFIX)gcc
|
||||||
|
COPY := $(PREFIX)objcopy
|
||||||
|
SIZE := $(PREFIX)size
|
||||||
|
GDB := $(PREFIX)gdb
|
||||||
|
|
||||||
|
OCD := openocd
|
||||||
|
|
||||||
|
NAME := application
|
||||||
|
|
||||||
|
OPT_LEVEL_CC := -g \
|
||||||
|
-O0
|
||||||
|
|
||||||
|
TARGET_FLAGS := -mcpu=cortex-m4 \
|
||||||
|
-mthumb \
|
||||||
|
-mfpu=fpv4-sp-d16 \
|
||||||
|
-mfloat-abi=hard
|
||||||
|
|
||||||
|
WG := -Wall \
|
||||||
|
-Wextra \
|
||||||
|
-Wpedantic \
|
||||||
|
-Werror
|
||||||
|
|
||||||
|
WCC := -Wconversion \
|
||||||
|
-Wsign-conversion \
|
||||||
|
-Wsign-compare \
|
||||||
|
-Wcast-align=strict \
|
||||||
|
-Wfloat-equal \
|
||||||
|
-Wlogical-op
|
||||||
|
|
||||||
|
AS_FLAGS := $(TARGET_FLAGS) \
|
||||||
|
$(OPT_LEVEL_CC) \
|
||||||
|
$(VERB_LEVEL) \
|
||||||
|
$(WG) \
|
||||||
|
-Wa,-a,-ad,-alms=$(BIN_DIR)/$(NAME).lst \
|
||||||
|
-x assembler-with-cpp
|
||||||
|
|
||||||
|
C_DEF := $(addprefix -D, $(C_DEF))
|
||||||
|
C_INC := $(addprefix -I, $(C_INC))
|
||||||
|
C_FLAGS := $(TARGET_FLAGS) \
|
||||||
|
$(OPT_LEVEL_CC) \
|
||||||
|
$(VERB_LEVEL) \
|
||||||
|
$(WG) \
|
||||||
|
$(WCC) \
|
||||||
|
-Wa,-a,-ad,-alms=$(BIN_DIR)/$(NAME).lst \
|
||||||
|
$(C_DEF) \
|
||||||
|
$(C_INC)
|
||||||
|
|
||||||
|
FLAGS_FILE := ./compile_flags.txt
|
||||||
|
|
||||||
|
LST_DIR := $(BIN_DIR)/list-files
|
||||||
|
|
||||||
|
LIBCTBOARD := $(LIB_DIR)/lib/libctboard.a
|
||||||
|
LIB_PATHS += $(LIB_DIR)/lib
|
||||||
|
LIB_NAMES += ctboard
|
||||||
|
|
||||||
|
LINKER_FLAGS := $(TARGET_FLAGS) \
|
||||||
|
-static \
|
||||||
|
-nostartfiles \
|
||||||
|
$(addprefix -L, $(LIB_PATHS)) \
|
||||||
|
-lm \
|
||||||
|
-lnosys \
|
||||||
|
-Wl,--start-group \
|
||||||
|
$(addprefix -l, $(LIB_NAMES)) \
|
||||||
|
-Wl,--end-group \
|
||||||
|
-specs=nosys.specs \
|
||||||
|
-specs=nano.specs \
|
||||||
|
-Wl,-Map,$(BIN_DIR)/$(NAME).map \
|
||||||
|
-Wl,-e,Reset_Handler \
|
||||||
|
-Wl,--gc-sections
|
||||||
|
|
||||||
|
LD_SCRIPT := $(LIB_DIR)/lib/linker-script.ld
|
||||||
|
GDB_INIT := $(COMMON_DIR)/.gdbinit
|
||||||
|
OCD_CONFIG := $(COMMON_DIR)/openocd.cfg
|
||||||
|
|
||||||
|
OBJ := $(addprefix $(OBJ_DIR)/, $(AS_SRC:.s=.o)) \
|
||||||
|
$(addprefix $(OBJ_DIR)/, $(C_SRC:.c=.o))
|
||||||
|
|
||||||
|
ELF := $(BIN_DIR)/$(NAME).elf
|
||||||
|
BIN := $(basename $(ELF)).bin
|
||||||
|
|
||||||
|
MKDIR := $(OBJ_DIR) \
|
||||||
|
$(BIN_DIR) \
|
||||||
|
$(LST_DIR)
|
||||||
|
|
||||||
|
VPATH := $(SRC_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: all clean clean_all dbgsrv flash launchgdb rebuild test
|
||||||
|
|
||||||
|
|
||||||
|
all: $(LIBCTBOARD) $(MKDIR) $(BIN) $(FLAGS_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
flash: $(MKDIR) $(BIN)
|
||||||
|
-$(OCD) -f $(OCD_CONFIG) \
|
||||||
|
-c "program $(BIN) verify exit 0x08000000"
|
||||||
|
|
||||||
|
|
||||||
|
dbgsrv: flash
|
||||||
|
-$(OCD) -f $(OCD_CONFIG) \
|
||||||
|
-c "gdb_port 4500" \
|
||||||
|
-c "init; reset run;"
|
||||||
|
|
||||||
|
|
||||||
|
launchgdb: $(ELF) $(BIN) $(GDB_INIT)
|
||||||
|
$(GDB) --command=$(GDB_INIT) $<
|
||||||
|
|
||||||
|
|
||||||
|
$(MKDIR):
|
||||||
|
mkdir -p $@
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# The sections have to correspond with the
|
||||||
|
# sections defined in the linker script who
|
||||||
|
# are directed to be located in the flash
|
||||||
|
# memory area.
|
||||||
|
#
|
||||||
|
$(BIN): $(ELF)
|
||||||
|
$(COPY) --output-target=binary \
|
||||||
|
-j .isr_vector \
|
||||||
|
-j .stack \
|
||||||
|
-j .heap \
|
||||||
|
-j .text \
|
||||||
|
-j .my_code \
|
||||||
|
-j .my_const \
|
||||||
|
-j .my_var \
|
||||||
|
-j .rodata\
|
||||||
|
-j .ARM.extab \
|
||||||
|
-j .ARM \
|
||||||
|
-j .preinit_array \
|
||||||
|
-j .init_array \
|
||||||
|
-j .fini_array \
|
||||||
|
-j .data \
|
||||||
|
-j .ccmram \
|
||||||
|
-j .ARM.extab \
|
||||||
|
-j .ARM \
|
||||||
|
$< $@
|
||||||
|
chmod -x $@
|
||||||
|
@printf "\n"
|
||||||
|
|
||||||
|
|
||||||
|
$(ELF): $(OBJ)
|
||||||
|
$(CC) -o $@ $^ $(LINKER_FLAGS) -T $(LD_SCRIPT)
|
||||||
|
chmod -x $@
|
||||||
|
|
||||||
|
|
||||||
|
$(OBJ_DIR)/%.o: %.s
|
||||||
|
$(CC) \
|
||||||
|
$(AS_FLAGS) \
|
||||||
|
-Wa,-a,-ad,-alms=$(LST_DIR)/$*.lst \
|
||||||
|
-c $< \
|
||||||
|
-o $@
|
||||||
|
|
||||||
|
|
||||||
|
$(OBJ_DIR)/%.o: %.c
|
||||||
|
$(CC) \
|
||||||
|
$(C_FLAGS) \
|
||||||
|
-Wa,-a,-ad,-alms=$(LST_DIR)/$*.lst \
|
||||||
|
-c $< \
|
||||||
|
-o $@
|
||||||
|
|
||||||
|
|
||||||
|
$(LIBCTBOARD):
|
||||||
|
echo "$(LIB_DIR)"
|
||||||
|
make -C "$(LIB_DIR)" rebuild
|
||||||
|
|
||||||
|
|
||||||
|
# this is to satisfy clangd (lsp client)
|
||||||
|
$(FLAGS_FILE):
|
||||||
|
@echo "$(C_FLAGS)" | sed 's/ /\n/g' > $@
|
||||||
|
|
||||||
|
|
||||||
|
clean:
|
||||||
|
-rm -rf $(MKDIR) $(FLAGS_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
clean_all:: clean
|
||||||
|
|
||||||
|
|
||||||
|
rebuild: clean all
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user