Merge branch 'master' into ADD-event-20250424-02-01-ab02ed313025c09c
All checks were successful
continuous-integration/drone/pr Build is passing

This commit is contained in:
tobiasd 2025-04-24 13:59:28 +00:00
commit 532cc30703
85 changed files with 2648 additions and 2562 deletions

View File

@ -7,12 +7,16 @@ clone:
depth: 150
steps:
- name: checks
- name: check-python
image: ghcr.io/astral-sh/ruff:latest
command: [ "check", "." ]
- name: check-custom
image: debian:bookworm
commands:
- apt update
# Install required packages
- apt install --yes coreutils sed grep libxml2-utils git findutils perl-base file mediainfo curl
- apt install --yes --no-install-recommends coreutils sed grep libxml2-utils git findutils perl-base file mediainfo curl
# Check whether non-EN news item would appear on front-page
- bash tools/check-non-en-frontpage.sh news
# Run pre-commit checks
@ -20,6 +24,61 @@ steps:
# Check syntax for all files as a safety net
- find . -type f \( -iname "*.xhtml" -o -iname "*.xml" -o -iname "*.xsl" \) -exec xmllint --noout {} +
- name: deploy-master
image: docker:27.4.1
environment:
# Environment variables necessary for rootless Docker
XDG_RUNTIME_DIR: "/run/user/1001"
DOCKER_HOST: "unix:///run/user/1001/docker.sock"
# Target bunsen directly, and use ipv4 proxies for noddack and gahn, as ipv6 broken.
TARGET: "www@bunsen.fsfeurope.org:fsfe.org/global/,www@proxy.noris.fsfeurope.org:fsfe.org/global/?10322,www@proxy.plutex.fsfeurope.org:fsfe.org/global/?10322"
KEY_PRIVATE:
from_secret: KEY_PRIVATE
KEY_PASSWORD:
from_secret: KEY_PASSWORD
GIT_TOKEN:
from_secret: BUILD_TOKEN
volumes:
# Mounting Docker socket of rootless docker user
- name: dockersock
path: /run/user/1001/docker.sock
commands:
- docker ps && echo "tampered with"
- docker compose -p fsfe-website run --remove-orphans --build build --target "$TARGET"
when:
branch:
- master
event:
exclude:
- pull_request
- name: deploy-test
image: docker:27.4.1
environment:
# Environment variables necessary for rootless Docker
XDG_RUNTIME_DIR: "/run/user/1001"
DOCKER_HOST: "unix:///run/user/1001/docker.sock"
# Target bunsen directly, and use ipv4 proxies for noddack and gahn, as ipv6 broken.
TARGET: "www@bunsen.fsfeurope.org:test.fsfe.org/global/,www@proxy.noris.fsfeurope.org:test.fsfe.org/global/?10322,www@proxy.plutex.fsfeurope.org:test.fsfe.org/global/?10322"
KEY_PRIVATE:
from_secret: KEY_PRIVATE
KEY_PASSWORD:
from_secret: KEY_PASSWORD
GIT_TOKEN:
from_secret: BUILD_TOKEN
volumes:
# Mounting Docker socket of rootless docker user
- name: dockersock
path: /run/user/1001/docker.sock
commands:
- docker ps && echo "tampered with"
- docker compose -p fsfe-website run --remove-orphans --build build --target "$TARGET"
when:
branch:
- test
event:
exclude:
- pull_request
trigger:
branch:
- master
@ -27,8 +86,17 @@ trigger:
event:
- push
- pull_request
node:
cont2: noris
volumes:
# Define Docker socket of rootless docker user
- name: dockersock
host:
path: /run/user/1001/docker.sock
---
kind: signature
hmac: 4c0dd0f272458d12234c72f66c4d420069591cac83819644df3c03a280102ded
hmac: 9d36b88284a9a0370d1718c9c6ffdb66127bbd8781744db2a57abdafd3f2d895
...

13
.gitignore vendored
View File

@ -13,11 +13,22 @@ global/data/topbanner/.topbanner.??.xml
.localmenu.*.xml
.*.xmllist
fsfe.org/search/index.js
fsfe.org/tags/tagged-*.en.xhtml
fsfe.org/tags/tagged-*.??.xhtml
fsfe.org/tags/.tags.??.xml
global/data/modules/fsfe-activities-options.en.xml
# Local build stuff
output
# Python venv
.venv
__pycache__
#Nltk
.nltk_data
## Status dir stuff
status.fsfe.org/*/data*/*
# Secrets
# docker compose
.env
# drone
secrets.txt

View File

@ -1,23 +1,34 @@
FROM debian:bookworm-slim
FROM debian:latest
# Install deps
RUN apt update
# Install required packages
RUN apt install --yes \
bash \
coreutils \
RUN apt install --yes --no-install-recommends \
rsync \
xsltproc \
libxml2-utils \
sed \
findutils \
grep \
make \
libc-bin \
wget \
procps \
libxslt1.1 \
libxml2 \
golang \
python3 \
python3-bs4
WORKDIR /fsfe-websites
ENTRYPOINT ["bash", "./build.sh" ]
python3-venv \
python3-pip \
git \
node-less \
openssh-client \
expect
# Setup venv
ENV VIRTUAL_ENV=/opt/venv
RUN python3 -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
# Copy the requirements
# Done in a seperate step for optimal docker caching
COPY ./requirements.txt /website-source/requirements.txt
RUN pip install -r /website-source/requirements.txt
# Copy everything else
COPY . /website-source/
WORKDIR /website-source
ENTRYPOINT [ "bash", "./entrypoint.sh" ]

157
Makefile
View File

@ -1,157 +0,0 @@
# -----------------------------------------------------------------------------
# Makefile for FSFE website build, phase 1
# -----------------------------------------------------------------------------
# This Makefile is executed in the root of the source directory tree, and
# creates some .xml and xhtml files as well as some symlinks, all of which
# serve as input files in phase 2. The whole phase 1 runs within the source
# directory tree and does not touch the target directory tree at all.
# -----------------------------------------------------------------------------
.PHONY: all .FORCE
.FORCE:
# This will be overwritten in the command line running this Makefile.
build_env = development
languages = none
# -----------------------------------------------------------------------------
# Build search index
# -----------------------------------------------------------------------------
# This step runs a Python tool that creates an index of all news and
# articles. It extracts titles, teaser, tags, dates and potentially more.
# The result will be fed into a JS file.
.PHONY: searchindex
all: searchindex
searchindex:
python3 tools/index-website.py
# -----------------------------------------------------------------------------
# Update CSS files
# -----------------------------------------------------------------------------
# This step recompiles the less files into the final CSS files to be
# distributed to the web server.
ifneq ($(build_env),development)
websites:=$(shell find . -mindepth 2 -maxdepth 2 -type d -regex "./[a-z\.]+\.[a-z]+/look")
all: $(foreach dir,$(websites), $(dir)/fsfe.min.css $(dir)/valentine.min.css)
$(dir $@)%.min.css: $(shell find $(dir $@) -name '*.less')
echo "* Compiling $@"
lessc "$*.less" -x "$@"
endif
# -----------------------------------------------------------------------------
# Update XSL stylesheets
# -----------------------------------------------------------------------------
# This step updates (actually: just touches) all XSL files which depend on
# another XSL file that has changed since the last build run. The phase 2
# Makefile then only has to consider the directly used stylesheet as a
# prerequisite for building each file and doesn't have to worry about other
# stylesheets imported into that one.
# This must run before the "dive into subdirectories" step, because in the news
# and events directories, the XSL files, if updated, will be copied for the
# per-year archives.
.PHONY: stylesheets
all: stylesheets
stylesheets: $(SUBDIRS)
tools/update_stylesheets.sh
# -----------------------------------------------------------------------------
# Dive into subdirectories
# -----------------------------------------------------------------------------
SUBDIRS := $(shell find . -regex "./[a-z\.]+\.[a-z]+/.*/Makefile" | xargs dirname)
all: $(SUBDIRS)
$(SUBDIRS): .FORCE
echo "* Preparing subdirectory $@"
$(MAKE) --silent --directory=$@ languages="$(languages)"
# -----------------------------------------------------------------------------
# Create XML symlinks
# -----------------------------------------------------------------------------
# After this step, the following symlinks will exist:
# * global/data/texts/.texts.<lang>.xml for each language
# * global/data/topbanner/.topbanner.<lang>.xml for each language
# Each of these symlinks will point to the corresponding file without a dot at
# the beginning of the filename, if present, and to the English version
# otherwise. This symlinks make sure that phase 2 can easily use the right file
# for each language, also as a prerequisite in the Makefile.
TEXTS_LINKS := $(foreach lang,$(languages),global/data/texts/.texts.$(lang).xml)
all: $(TEXTS_LINKS)
global/data/texts/.texts.%.xml: .FORCE
if [ -f global/data/texts/texts.$*.xml ]; then \
ln -sf texts.$*.xml $@; \
else \
ln -sf texts.en.xml $@; \
fi
TOPBANNER_LINKS := $(foreach lang,$(languages),global/data/topbanner/.topbanner.$(lang).xml)
all: $(TOPBANNER_LINKS)
global/data/topbanner/.topbanner.%.xml: .FORCE
if [ -f global/data/topbanner/topbanner.$*.xml ]; then \
ln -sf topbanner.$*.xml $@; \
else \
ln -sf topbanner.en.xml $@; \
fi
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# The following steps are handled in an external script, because the list of
# files to generate is not known when the Makefile starts - some new tags might
# be introduced when generating the .xml files in the news/* subdirectories.
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# -----------------------------------------------------------------------------
# Create XSL symlinks
# -----------------------------------------------------------------------------
# After this step, each directory with source files for HTML pages contains a
# symlink named .default.xsl and pointing to the default.xsl "responsible" for
# this directory. These symlinks make it easier for the phase 2 Makefile to
# determine which XSL script should be used to build a HTML page from a source
# file.
.PHONY: default_xsl
all: default_xsl
default_xsl:
tools/update_defaultxsls.sh
# -----------------------------------------------------------------------------
# Update local menus
# -----------------------------------------------------------------------------
# After this step, all .localmenu.??.xml files will be up to date.
.PHONY: localmenus
all: localmenus
localmenus: $(SUBDIRS)
tools/update_localmenus.sh "$(languages)"
# -----------------------------------------------------------------------------
# Update XML filelists
# -----------------------------------------------------------------------------
# After this step, the following files will be up to date:
# * tags/tagged-<tags>.en.xhtml for each tag used. Apart from being
# automatically created, these are regular source files for HTML pages, and
# in phase 2 are built into pages listing all news items and events for a
# tag.
# * tags/.tags.??.xml with a list of the tags useed.
# * <dir>/.<base>.xmllist for each <dir>/<base>.sources as well as for each
# tags/tagged-<tags>.en.xhtml. These files are used in phase 2 to include the
# correct XML files when generating the HTML pages. It is taken care that
# these files are only updated whenever their content actually changes, so
# they can serve as a prerequisite in the phase 2 Makefile.
.PHONY: xmllists
all: xmllists
xmllists: $(SUBDIRS)
tools/update_xmllists.sh "$(languages)"

View File

@ -5,11 +5,11 @@ This repository contains the source files of [fsfe.org](https://fsfe.org), pdfre
## Table of Contents
* [Technical information](#technical-information)
* [Structure](#structure)
* [Contribute](#contribute)
* [Translate](#translate)
* [Build](#build)
- [Technical information](#technical-information)
- [Structure](#structure)
- [Contribute](#contribute)
- [Translate](#translate)
- [Build](#build)
## Technical information
@ -19,7 +19,7 @@ For information on how the build process works see [docs subfolder](./docs/overv
## Structure
Most files are XHTML files organised in a rather logical folder structure.
Most files are XHTML files organised in a rather logical folder structure.
Every website served using this repo has its own folder with the full domain name it is to be served from.
@ -27,42 +27,43 @@ Every website served using this repo has its own folder with the full domain nam
This repository also contains the source files of other websites the FSFE hosts:
* `fsfe.org` for [fsfe.org](http://fsfe.org)
* `activities/android` for [freeyourandroid.org](http://freeyourandroid.org)
* `activities/ilovefs` for [ilovefs.org](http://ilovefs.org)
* `drm.info` for [drm.info](http://drm.info)
* `pdfreaders.org` for [pdfreaders.org](http://pdfreaders.org)
* [test.fsfe.org](https://test.fsfe.org) is fsfe.org built from the test branch of this repository
- `fsfe.org` for [fsfe.org](http://fsfe.org)
- `activities/android` for [freeyourandroid.org](http://freeyourandroid.org)
- `activities/ilovefs` for [ilovefs.org](http://ilovefs.org)
- `drm.info` for [drm.info](http://drm.info)
- `pdfreaders.org` for [pdfreaders.org](http://pdfreaders.org)
- [test.fsfe.org](https://test.fsfe.org) is fsfe.org built from the test branch of this repository
### Important folders
Notable top level directories are:
* `build`: Mostly custom Bash and XSL scripts to build the website
* `global`: Globally used data files and modules, also the static translated strings.
* `tools`: Contains miscellaneous XML, XSL, and SH files.
- `build`: Mostly custom Bash and XSL scripts to build the website
- `global`: Globally used data files and modules, also the static translated strings.
- `tools`: Contains miscellaneous XML, XSL, and SH files.
And of course the different website folders.
And here are dome notable directories inside the folder for the main webpage, fsfe.org.
* `about`: Information about the FSFE itself, its team members etc
* `activities`: All specific FSFE activities
* `at`, `de`, `ee` etc: Folders used for the FSFE country teams
* `cgi-bin`: Our very few CGI scripts
* `error`: Custom 4xx and 5xx error pages
* `events`: Files for our events, ordered by year
* `freesoftware`: More timeless pages explaining Free Software and related topics
* `graphics`: Icons, pictures and logos
* `internal`: Forms used mostly by FSFE staff for internal processes
* `look`: CSS and other style files
* `news`: Files for news articles, press releases, and newsletters ordered by year
* `order`: Our web shop
* `scripts`: JavaScript files used on our pages
* `tags`: Files necessary to display used tags throughout the website. Mostly automatically generated
- `about`: Information about the FSFE itself, its team members etc
- `activities`: All specific FSFE activities
- `at`, `de`, `ee` etc: Folders used for the FSFE country teams
- `cgi-bin`: Our very few CGI scripts
- `error`: Custom 4xx and 5xx error pages
- `events`: Files for our events, ordered by year
- `freesoftware`: More timeless pages explaining Free Software and related topics
- `graphics`: Icons, pictures and logos
- `internal`: Forms used mostly by FSFE staff for internal processes
- `look`: CSS and other style files
- `news`: Files for news articles, press releases, and newsletters ordered by year
- `order`: Our web shop
- `scripts`: JavaScript files used on our pages
- `tags`: Files necessary to display used tags throughout the website. Mostly automatically generated
## Contribute
Become member of our awesome [webmaster team](https://fsfe.org/contribute/web/) and help to improve our online information platform!
Become member of our awesome [webmaster team](https://fsfe.org/contribute/web/) and help to improve our online information platform!
## Translate
@ -79,30 +80,40 @@ There are two ways to build and develop the directory locally. Initial builds of
Alterations to build scripts or the files used site-wide will result in near full rebuilds.
### Native
We can either install the required dependencies manually using our preferred package manager. If you are a nix use one can run `nix-shell` to enter a shell with the required build dependencies.
The required binary names are
We can either install the required dependencies manually using our preferred package manager. If you are a nix use one can run `nix-shell` to enter a shell with the required build dependencies, with the python `virtualenv` already installed and activated.
If installing manually, the required binary names are
```
realpath rsync xsltproc xmllint sed find egrep grep wc make tee date iconv wget shuf python3
python3 pip
```
The package names for Debian, are
Also needed are the libraries
```
bash bash-completion coreutils diffutils findutils inotify-tools libxml2-utils libxslt make procps python3 rsync
libxml2 libxslt
```
Then, we must activate a Python virtual env and install the python dependencies.
```
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
After getting the dependencies one way or another we can actually build and serve the pages.
The pages can be built and served by running `./build.sh`. Try `--help` for more information. The simple web server used lacks the features of `apache` which used on the FSFE web servers. This is why no index is automatically selected form and directory and other behaviors.
The pages can be built and served by running `./build.py`. Try `--help` for more information. The simple web server used lacks the features of `apache` which used on the FSFE web servers. This is why no index is automatically selected for each directory and other behaviours.
### Docker
Simply running `docker compose run --service-ports build --serve` should build the webpages and make them available over localhost.
Some more explanation: we are essentially just using docker as a way to provide dependencies and then running the build script. All flags after `build` are passed to `build.sh`. The `service-ports` flag is required to open ports from the container for serving the output, not needed if not using the `--serve` flag of the build script.
Please note that files generated during the build process using docker are owned by root. This does not cause issues unless you with to manually alter the output or switch to native building instead of docker.
If you wish to switch to native building after using docker, you must use `sudo git clean -fdx` to remove the files generated using docker.
Some more explanation: we are essentially just using docker as a way to provide dependencies and then running the build script. All flags after `build` are passed to `build.py`. The `service-ports` flag is required to open ports from the container for serving the output, not needed if not using the `--serve` flag of the build script.
## Testing
While most small changes can be tested adequately by building locally some larger changes, particularly ones relating to the order pages, event registration and other forms may require more integrated testing. This can be achieved using the `test` branch. This branch is built and served in the same way as the main site, [fsfe.org](https://fsfe.org). The built version of the `test` branch may be viewed at [test.fsfe.org](https://test.fsfe.org).
## Status Viewing

122
build.py Executable file
View File

@ -0,0 +1,122 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import argparse
import logging
import multiprocessing
import os
from pathlib import Path
from build.phase0.full import full
from build.phase1.run import phase1_run
from build.phase2.run import phase2_run
from build.phase3.serve_websites import serve_websites
from build.phase3.stage_to_target import stage_to_target
logger = logging.getLogger(__name__)
def parse_arguments() -> argparse.Namespace:
"""
Parse the arguments of the website build process
"""
parser = argparse.ArgumentParser(
description="Python script to handle building of the fsfe webpage"
)
parser.add_argument(
"--target",
dest="target",
help="Final dirs for websites to be build to. Can be a single path, or a comma separated list of valid rsync targets. Supports custom rsynx extension for specifying ports for ssh targets, name@host:path?port.",
type=str,
default="./output/final",
)
parser.add_argument(
"--log-level",
dest="log_level",
type=str,
default="INFO",
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
help="Set the logging level (default: INFO)",
)
parser.add_argument(
"--full",
dest="full",
help="Force a full rebuild of all webpages.",
action="store_true",
)
parser.add_argument(
"--processes",
dest="processes",
help="Number of processes to use when building the website",
type=int,
default=multiprocessing.cpu_count(),
)
parser.add_argument(
"--languages",
dest="languages",
help="Languages to build website in.",
default=list(
map(lambda path: path.name, Path(".").glob("global/languages/??"))
),
type=lambda input: input.split(","),
)
parser.add_argument(
"--stage",
dest="stage",
help="Force the use of an internal staging directory.",
action="store_true",
)
parser.add_argument(
"--serve",
dest="serve",
help="Serve the webpages after rebuild",
action="store_true",
)
args = parser.parse_args()
return args
def main(args: argparse.Namespace):
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
level=args.log_level,
)
logger.debug(args)
with multiprocessing.Pool(args.processes) as pool:
logger.info("Starting phase 0 - Conditional Setup")
# TODO Should also be triggered whenever any build python file is changed
if args.full:
full()
stage_required = any(
[args.stage, "@" in args.target, ":" in args.target, "," in args.target]
)
working_target = Path("./output/stage" if stage_required else args.target)
# Processes needed only for subdir stuff
phase1_run(args.languages, args.processes, pool)
phase2_run(args.languages, pool, working_target)
logger.info("Starting Phase 3 - Conditional Finishing")
if stage_required:
stage_to_target(working_target, args.target, pool)
if args.serve:
serve_websites(working_target, 2000, 100)
if __name__ == "__main__":
"""
Main process of the website builder
"""
# Change to the dir the script is in.
os.chdir(os.path.dirname(__file__))
args = parse_arguments()
main(args)

View File

@ -1,48 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<-EOF
# build.sh Usage
## General
This script is a wrapper script over ./build/build_main.sh that provides nicer option names, and the options to serve the files.
For documentation on the build script itself see ./build/README.md
## Flags
### -f | --full
Perform a full rebuild of the webpages.
### -s | --serve
Serve the build webpages over localhost.
### --
Everything after this is passed directly to build_main.
See ./build/README.md for valid options.
EOF
exit 1
}
command="build_run"
serve=""
extra_args=""
while [ "$#" -gt 0 ]; do
case "$1" in
--full | -f)
command="build_into" && shift 1
;;
--serve | -s)
serve="true" && shift 1
;;
--)
shift 1
while [ "$#" -gt 0 ]; do
extra_args+="$1 "
shift 1
done
;;
*)
usage
;;
esac
done
mkdir -p ./output
./build/build_main.sh "$command" ./output/final --statusdir ./output/final/status.fsfe.org/fsfe.org/data $extra_args
if [[ "$serve" ]]; then
python3 ./serve-websites.py
fi

View File

@ -1,42 +0,0 @@
# Main Commands
Note that targets takes a comma separated list of valid rsync targets, and hence supports ssh targets. If targeting more than one directory one must use the --stage-dir flag documented below.
## build_main.sh [options] build_run "targets"
Perform the page build. Write output to targets. The source directory is determined from the build scripts own location.
## build_main.sh [options] git_build_into "targets"
Update repo to latest version of upstream branch and then perform a standard build. Write output to targets. The source directory is determined from the build scripts own location.
## build_main.sh [options] build_into "targets"
Perform a full rebuild of the webpages, removing all cached files. Write output to targets. The source directory is determined from the build scripts own location.
# Internal Commands
It is unlikely that you will need to directly call these commands, but they are documented here never the less.
## build_main.sh [options] build_xmlstream "file.xhtml"
Compile an xml stream from the specified file, additional sources will be determined and included automatically. The stream is suitable for being passed into xsltproc.
## build_main.sh [options] process_file "file.xhtml" [processor.xsl]
Generate output from an xhtml file as if it would be processed during the
build. Output is written to STDOUT and can be redirected as desired.
If a xslt file is not given, it will be chosen automatically.
## build_main.sh [options] tree_maker [input_dir] "targets"
Generate a set of make rules to build the website contained in input_dir. targets should be the www root of a web server. If input_dir is omitted, it will be the source directory determined from the build scripts location. Note: if targets is set via previous options, and only one parameter is given, then this parameter will be interpreted as input_dir
# OPTIONS
## --source "source_dir"
Force a specific source directory. If not explicitly given source_dir is determined from the build scripts own location. Paths given in .sources files are interpreted as relative to source_dir making this option useful when building a webpage outside of the build scripts "regular" tree.
## --status-dir "status_dir"
A directory to which messages are written. If no status_dir is provided information will be written to stdout. The directory will also be used to store some temporary files, which would otherwise be set up in the system wide temp directory.
## --stage-dir "stage_dir"
Directory used for staging the website builds. The websites are first build into this directory, then copied to each targets.
## --build-env "selection"
Indicate the current build environment. "selection" can be one of: * "fsfe.org": building https://fsfe.org on the production server * "test.fsfe.org": building https://test.fsfe.org on the production server * "development" (default): local development build In a local development build, code to dynamically compile the less files into CSS will be included in the HTML output, while in the other environments, the precompiles fsfe.min.css (or valentine.min.css) will be referenced from the generated web pages.
## --languages "languages"
Takes a comma separated list of language shot codes to build the website in. For example, to build the site in English and French only one would use `--languages en,fr`. One of the built languages must be English.
## --help
Show this README.

6
build/__init__.py Normal file
View File

@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
# __init__.py is a special Python file that allows a directory to become
# a Python package so it can be accessed using the 'import' statement.

View File

@ -1,94 +0,0 @@
#!/usr/bin/env bash
[ -z "$inc_misc" ] && . "$basedir/build/misc.sh"
if [ -z "$inc_arguments" ]; then
inc_arguments=true
basedir="$(realpath "${0%/*}/..")"
while [ "$#" -gt 0 ]; do
case "$1" in
-s | --statusdir | --status-dir)
[ "$#" -gt 0 ] && shift 1 && statusdir="$1"
;;
--source)
[ "$#" -gt 0 ] && shift 1 && basedir="$1"
;;
--stage | --stagedir | --stage-dir)
[ "$#" -gt 0 ] && shift 1 && stagedir="$1"
;;
--build-env)
[ "$#" -gt 0 ] && shift 1 && build_env="$1"
;;
--languages)
[ "$#" -gt 0 ] && shift 1 && languages="$1"
;;
-h | --help)
command="help"
;;
build_into)
command="$1$command"
[ "$#" -gt 0 ] && shift 1 && target="$1"
;;
git_build_into)
command="$1$command"
[ "$#" -gt 0 ] && shift 1 && target="$1"
;;
build_run)
command="$1$command"
[ "$#" -gt 0 ] && shift 1 && target="$1"
;;
build_xmlstream)
command="$1$command"
[ "$#" -gt 0 ] && shift 1 && workfile="$1"
;;
tree_maker)
command="$1$command"
[ -n "$target" -o -n "$3" ] && shift 1 && tree="$1"
shift 1
[ -n "$1" ] && target="$1"
;;
process_file)
command="$1$command"
[ "$#" -gt 0 ] && shift 1 && workfile="$1"
[ "$#" -gt 0 ] && shift 1 && processor="$1"
;;
*)
print_error "Unknown option: $1"
exit 1
;;
esac
[ "$#" -gt 0 ] && shift 1
done
tree="${tree:-$basedir}"
stagedir="${stagedir:-$target}"
readonly tree="${tree:+$(realpath "$tree")}"
readonly stagedir="${stagedir:+$(realpath "$stagedir")}"
readonly basedir="${basedir:+$(realpath "$basedir")}"
readonly build_env="${build_env:-development}"
readonly command
if [ "$languages" ]; then
readonly languages="$(echo "$languages" | tr ',' ' ')"
else
readonly languages="$(ls -xw0 "${basedir}/global/languages")"
fi
if [ "$stagedir" != "$target" ] && printf %s "$target" | egrep -q '^.+@.+:(.+)?$'; then
readonly target
else
readonly target="${target:+$(realpath "$target")}"
fi
case "$command" in
build_into) [ -z "$target" ] && die "Missing destination directory" ;;
git_build_into) [ -z "$target" ] && die "Missing destination directory" ;;
build_run) [ -z "$target" ] && die "Missing destination directory" ;;
process_file) [ -z "$workfile" ] && die "Need at least input file" ;;
build_xmlstream) [ -z "$workfile" ] && die "Missing xhtml file name" ;;
tree_maker) [ -z "$target" ] && die "Missing target location" ;;
*help*)
cat "$basedir/build/README.md"
exit 0
;;
*) die "Urecognised command or no command given" ;;
esac
fi

View File

@ -1,82 +0,0 @@
#!/usr/bin/env bash
# Dependency check function
check_dependencies() {
depends="$@"
deperrors=''
for depend in $depends; do
if ! which "$depend" >/dev/null 2>&1; then
deperrors="$depend $deperrors"
fi
done
if [ -n "$deperrors" ]; then
printf '\033[1;31m'
cat <<-EOF
The build script depends on some other programs to function.
Not all of those programs could be located on your system.
Please use your package manager to install the following programs:
EOF
printf '\n\033[0;31m%s\n' "$deperrors"
exit 1
fi 1>&2
}
# Check dependencies for all kinds of build envs (e.g. development, fsfe.org)
check_dependencies realpath rsync xsltproc xmllint sed find egrep grep wc make tee date iconv wget shuf python3
if ! make --version | grep -q "GNU Make 4"; then
echo "The build script requires GNU Make 4.x"
exit 1
fi
basedir="${0%/*}/.."
[ -z "$inc_misc" ] && . "$basedir/build/misc.sh"
readonly start_time="$(date +%s)"
. "$basedir/build/arguments.sh"
# Check special dependencies for (test.)fsfe.org build server
if [ "$build_env" == "fsfe.org" ] || [ "$build_env" == "test.fsfe.org" ]; then
check_dependencies lessc
fi
statusdir="${statusdir/#\~/$HOME}"
if [ -n "$statusdir" ]; then
mkdir -p "$statusdir"
[ ! -w "$statusdir" -o ! -d "$statusdir" ] &&
die "Unable to set up status directory in \"$statusdir\",\n" \
"either select a status directory that exists and is writable,\n" \
"or run the build script without output to a status directory"
fi
readonly statusdir="${statusdir:+$(realpath "$statusdir")}"
buildpids=$(
ps -eo command |
egrep "[s]h ${0} .*" |
wc -l
)
if [ $command = "build_into" -o $command = "git_build_into" ] && [ "$buildpids" -gt 2 ]; then
debug "build script is already running"
exit 1
fi
[ -z "$inc_filenames" ] && . "$basedir/build/filenames.sh"
[ -z "$inc_buildrun" ] && . "$basedir/build/buildrun.sh"
[ -z "$inc_makerules" ] && . "$basedir/build/makerules.sh"
[ -z "$inc_processor" ] && . "$basedir/build/processor.sh"
[ -z "$inc_scaffold" ] && . "$basedir/build/scaffold.sh"
case "$command" in
git_build_into) if [ -f "${statusdir}/full_build" ]; then
debug "discovered flag file, performing full build"
rm "${statusdir}/full_build"
build_into
else
git_build_into
fi ;;
build_into) build_into ;;
build_run) buildrun ;;
process_file) process_file "$workfile" "$processor" ;;
build_xmlstream) build_xmlstream "$(get_shortname "$workfile")" "$(get_language "$workfile")" ;;
tree_maker) tree_maker "$tree" "$target" ;;
esac

View File

@ -1,140 +0,0 @@
#!/usr/bin/env bash
inc_buildrun=true
[ -z "$inc_makerules" ] && . "$basedir/build/makerules.sh"
[ -z "$inc_logging" ] && . "$basedir/build/logging.sh"
[ -z "$inc_misc" ] && . "$basedir/build/misc.sh"
match() {
printf %s "$1" | egrep -q "$2"
}
dir_maker() {
# set up directory tree for output
# optimise by only issuing mkdir commands
# for leaf directories
input="${1%/}"
output="${2%/}"
curpath="$output"
find "$input" -depth -type d \
-regex "$input/[a-z\.]+\.[a-z]+\(/.*\)?" \
-printf '%P\n' |
while read -r filepath; do
oldpath="$curpath"
curpath="$output/$filepath/"
match "$oldpath" "^$curpath" || mkdir -p "$curpath"
done
}
# The actual build
buildrun() {
set -o pipefail
printf %s "$start_time" >"$(logname start_time)"
ncpu="$(grep -c ^processor /proc/cpuinfo)"
[ -f "$(logname lasterror)" ] && rm "$(logname lasterror)"
[ -f "$(logname debug)" ] && rm "$(logname debug)"
{
echo "Starting phase 1" &&
make --silent --directory="$basedir" build_env="${build_env}" languages="$languages" 2>&1 &&
echo "Finishing phase 1" ||
die "Error during phase 1"
} | t_logstatus phase_1 || exit 1
dir_maker "$basedir" "$stagedir" || exit 1
forcelog Makefile
{
tree_maker "$basedir" "$stagedir" 2>&1 ||
die "Error during phase 2 Makefile generation"
} >"$(logname Makefile)" || exit 1
{
echo "Starting phase 2" &&
make --silent --jobs=$ncpu --file="$(logname Makefile)" 2>&1 &&
echo "Finishing phase 2" ||
die "Error during phase 2"
} | t_logstatus phase_2 || exit 1
if [ "$stagedir" != "$target" ]; then
# rsync issues a "copying unsafe symlink" message for each of the "unsafe"
# symlinks which we copy while rsyncing. These messages are issued even if
# the files have not changed and clutter up the output, so we filter them
# out.
{
for destination in ${target//,/ }; do
echo "Syncing files to $(echo "$destination" | grep -Po "(?<=@)[^:]+")"
rsync -av --copy-unsafe-links --del --exclude "status.fsfe.org/*fsfe.org/data" "$stagedir/" "$destination/" | grep -v "copying unsafe symlink"
done
} | t_logstatus stagesync
fi
date +%s >"$(logname end_time)"
if [ -n "$statusdir" ]; then
(
cd "$statusdir"/..
./index.cgi | tail -n+3 >"$statusdir"/status_$(date +%s).html
)
fi
}
# Update git (try 3x) and then do an actual build
git_build_into() {
forcelog GITchanges
GITchanges="$(logname GITchanges)"
forcelog GITerrors
GITerrors="$(logname GITerrors)"
gitterm=1
i=0
while [[ ($gitterm -ne 0) && ($i -lt 3) ]]; do
((i++))
git -C "$basedir" pull >"$GITchanges" 2>"$GITerrors"
gitterm="$?"
if [ $gitterm -ne 0 ]; then
debug "Git pull unsuccessful. Trying again in a few seconds."
sleep $(shuf -i 10-30 -n1)
fi
done
if [ "$gitterm" -ne 0 ]; then
debug "Three git pulls failed, hard resetting and repulling"
git -C "$basedir" reset --hard HEAD~50 >"$GITchanges" 2>"$GITerrors"
git -C "$basedir" pull >>"$GITchanges" 2>>"$GITerrors"
gitterm="$?"
fi
if [ "$gitterm" -ne 0 ]; then
die "GIT reported the following problem:\n$(cat "$GITerrors")"
fi
if egrep '^Already up[ -]to[ -]date' "$GITchanges"; then
debug "No changes to GIT:\n$(cat "$GITchanges")"
# Exit status should only be 0 if there was a successful build.
# So set it to 1 here.
exit 1
fi
logstatus GITlatest <"$GITchanges"
buildrun
}
# Clean up everything and then do an actual (full) build
build_into() {
# Clean up source directory.
git -C "${basedir}" clean -dxf --exclude=status.fsfe.org/translations/data
# Remove old stage directory.
rm -rf "${stagedir}"
buildrun
}

View File

@ -1,16 +0,0 @@
#!/usr/bin/env bash
inc_filenames=true
get_language() {
# extract language indicator from a given file name
echo "$(echo "$1" | sed -r 's:^.*\.([a-z]{2})\.[^\.]+$:\1:')"
}
get_shortname() {
# get shortened version of a given file name
# required for internal processing
#echo "$(echo "$1" | sed -r 's:\.[a-z]{2}.xhtml$::')";
echo "${1%.??.xhtml}"
}

6
build/lib/__init__.py Normal file
View File

@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
# __init__.py is a special Python file that allows a directory to become
# a Python package so it can be accessed using the 'import' statement.

128
build/lib/misc.py Normal file
View File

@ -0,0 +1,128 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import subprocess
import sys
from pathlib import Path
import lxml.etree as etree
logger = logging.getLogger(__name__)
def keys_exists(element: dict, *keys: str) -> bool:
"""
Check if *keys (nested) exists in `element` (dict).
"""
if not isinstance(element, dict):
raise AttributeError("keys_exists() expects dict as first argument.")
if len(keys) == 0:
raise AttributeError("keys_exists() expects at least two arguments, one given.")
_element = element
for key in keys:
try:
_element = _element[key]
except KeyError:
return False
return True
def sort_dict(dict: dict) -> dict:
"""
Sort dict by keys
"""
return {key: val for key, val in sorted(dict.items(), key=lambda ele: ele[0])}
def update_if_changed(path: Path, content: str) -> None:
"""
Compare the content of the file at path with the content.
If the file does not exist,
or its contents does not match content,
write content to the file.
"""
if not path.exists() or path.read_text() != content:
logger.debug(f"Updating {path}")
path.write_text(content)
def touch_if_newer_dep(file: Path, deps: list[Path]) -> None:
"""
Takes a filepath , and a list of path of its dependencies.
If any of the dependencies has been altered more recently than the file,
touch the file.
Essentially simple reimplementation of make deps for build targets.
"""
if any(dep.stat().st_mtime > file.stat().st_mtime for dep in deps):
logger.info(f"Touching {file}")
file.touch()
def delete_file(file: Path) -> None:
"""
Delete given file using pathlib
"""
logger.debug(f"Removing file {file}")
file.unlink()
def lang_from_filename(file: Path) -> str:
"""
Get the lang code from a file, where the filename is of format
<name>.XX.<ending>, with xx being the lang code.
"""
lang = file.with_suffix("").suffix.removeprefix(".")
# Lang codes should be the iso 631 2 letter codes, but sometimes we use "nolang" to srop a file being built
if len(lang) != 2 and lang != "nolang":
logger.critical(
f"Language {lang} from file {file} not of correct length, exiting"
)
sys.exit(1)
else:
return lang
def run_command(commands: list) -> str:
result = subprocess.run(
commands,
capture_output=True,
# Get output as str instead of bytes
universal_newlines=True,
)
if result.returncode != 0:
logger.critical(f"Command {commands} failed with error")
logger.critical(result.stderr.strip())
sys.exit(1)
return result.stdout.strip()
def get_version(file: Path) -> int:
"""
Get the version tag of an xhtml|xml file
"""
xslt_tree = etree.parse(Path("build/xslt/get_version.xsl"))
transform = etree.XSLT(xslt_tree)
result = transform(etree.parse(file))
result = str(result).strip()
if result == "":
result = str(0)
logger.debug(f"Got version: {result}")
return int(result)
def get_basepath(file: Path) -> Path:
"""
Return the file with the last two suffixes removed
"""
return file.with_suffix("").with_suffix("")
def get_basename(file: Path) -> str:
"""
Return the name of the file with the last two suffixes removed
"""
return file.with_suffix("").with_suffix("").name

248
build/lib/process_file.py Normal file
View File

@ -0,0 +1,248 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import re
from datetime import datetime
from pathlib import Path
import lxml.etree as etree
from build.lib.misc import get_basename, get_version, lang_from_filename
logger = logging.getLogger(__name__)
def _include_xml(file: Path) -> str:
"""
include second level elements of a given XML file
this emulates the behaviour of the original
build script which wasn't able to load top
level elements from any file
"""
work_str = ""
if file.exists():
tree = etree.parse(file)
root = tree.getroot()
# Remove <version> because the filename attribute would otherwise be added
# to this element instead of the actual content element.
for elem in root.xpath("version"):
root.remove(elem)
# Iterate over all elements in root node, add a filename attribute and then append the string to work_str
for elem in root.xpath("*"):
elem.set("filename", get_basename(file))
work_str += etree.tostring(elem, encoding="utf-8").decode("utf-8")
return work_str
def _get_attributes(file: Path) -> str:
"""
get attributes of top level element in a given
XHTML file
"""
work_str = ""
tree = etree.parse(file)
root = tree.getroot()
attributes = root.attrib
for attrib in attributes:
work_str += f'{attrib}="{attributes[attrib]}"\n'
return work_str
def _list_langs(file: Path) -> str:
"""
list all languages a file exists in by globbing up
the shortname (i.e. file path with file ending omitted)
output is readily formatted for inclusion
in xml stream
"""
return "\n".join(
list(
map(
lambda path: (
f'<tr id="{lang_from_filename(path)}">'
+ (
Path(f"global/languages/{lang_from_filename(path)}")
.read_text()
.strip()
if Path(f"global/languages/{lang_from_filename(path)}").exists()
else lang_from_filename(path)
)
+ "</tr>"
),
file.parent.glob(f"{get_basename(file)}.??{file.suffix}"),
)
)
)
def _auto_sources(action_file: Path, lang: str) -> str:
"""
import elements from source files, add file name
attribute to first element included from each file
"""
work_str = ""
list_file = action_file.with_stem(
f".{action_file.with_suffix('').stem}"
).with_suffix(".xmllist")
if list_file.exists():
with list_file.open("r") as file:
for path in map(lambda line: Path(line.strip()), file):
path_xml = (
path.with_suffix(f".{lang}.xml")
if path.with_suffix(f".{lang}.xml").exists()
else path.with_suffix(".en.xml")
)
work_str += _include_xml(path_xml)
return work_str
def _build_xmlstream(infile: Path):
"""
assemble the xml stream for feeding into xsltproc
the expected shortname and language flag indicate
a single xhtml page to be built
"""
# TODO
# Ideally this would use lxml to construct an object instead of string templating.
# Should be a little faster, and also guarantees that its valid xml
logger.debug(f"infile: {infile}")
shortname = infile.with_suffix("")
lang = lang_from_filename(infile)
glob = infile.parent.joinpath(f"{get_basename(infile)}.??{infile.suffix}")
logger.debug(f"formed glob: {glob}")
lang_lst = list(
infile.parent.glob(f"{get_basename(infile)}.??{infile.suffix}"),
)
logger.debug(f"file lang list: {lang_lst}")
original_lang = (
"en"
if infile.with_suffix("").with_suffix(f".en{infile.suffix}").exists()
else sorted(
infile.parent.glob(f"{get_basename(infile)}.??{infile.suffix}"),
key=get_version,
reverse=True,
)[0]
.with_suffix("")
.suffix.removeprefix(".")
)
topbanner_xml = Path(f"global/data/topbanner/.topbanner.{lang}.xml")
texts_xml = Path(f"global/data/texts/.texts.{lang}.xml")
date = str(datetime.now().date())
# time = str(datetime.now().time())
action_lang = ""
translation_state = ""
if infile.exists():
action_lang = lang
original_version = get_version(
shortname.with_suffix(f".{original_lang}{infile.suffix}")
)
lang_version = get_version(shortname.with_suffix(f".{lang}{infile.suffix}"))
translation_state = (
"up-to-date"
if (original_version <= lang_version)
else (
"very-outdated"
if (original_version - 3 >= lang_version)
else "outdated"
)
)
else:
action_lang = original_lang
translation_state = "untranslated"
action_file = shortname.with_suffix(f".{action_lang}{infile.suffix}")
logger.debug(f"action_file: {action_file}")
result_str = f"""
<buildinfo
date="{date}"
original="{original_lang}"
filename="/{str(shortname.with_suffix("")).removeprefix("/")}"
fileurl="/{shortname.relative_to(shortname.parts[0]).with_suffix("")}"
dirname="/{shortname.parent}/"
language="{lang}"
translation_state="{translation_state}"
>
<trlist>
{_list_langs(infile)}
</trlist>
<topbanner>
{_include_xml(topbanner_xml)}
</topbanner>
<textsetbackup>
{_include_xml(Path("global/data/texts/texts.en.xml"))}
</textsetbackup>
<textset>
{_include_xml(texts_xml)}
</textset>
<document
language="{action_lang}"
{_get_attributes(action_file)}
>
<set>
{_auto_sources(action_file, lang)}
</set>
{_include_xml(action_file)}
</document>
</buildinfo>
"""
return result_str
def process_file(infile: Path, processor: Path) -> str:
"""
Process a given file using the correct xsl sheet
"""
logger.debug(f"Processing {infile}")
lang = lang_from_filename(infile)
xmlstream = _build_xmlstream(infile)
xslt_tree = etree.parse(processor.resolve())
transform = etree.XSLT(xslt_tree)
result = str(transform(etree.XML(xmlstream)))
# And now a bunch of regexes to fix some links.
# xx is the language code in all comments
# TODO
# Probably a faster way to do this
# Maybe iterating though all a tags with lxml?
# Once buildxmlstream generates an xml object that should be faster.
# Remove https://fsfe.org (or https://test.fsfe.org) from the start of all
result = re.sub(
r"""href\s*=\s*("|')(https?://(test\.)?fsfe\.org)([^>])\1""",
r"""href=\1\3\1""",
result,
flags=re.MULTILINE | re.IGNORECASE,
)
# Change links from /foo/bar.html into /foo/bar.xx.html
# Change links from foo/bar.html into foo/bar.xx.html
# Same for .rss and .ics links
result = re.sub(
r"""href\s*=\s*("|')(/?([^:>]+/)?[^:/.]+\.)(html|rss|ics)(#[^>]*)?\1""",
rf"""href=\1\2{lang}.\4\5\1""",
result,
flags=re.MULTILINE | re.IGNORECASE,
)
# Change links from /foo/bar/ into /foo/bar/index.xx.html
# Change links from foo/bar/ into foo/bar/index.xx.html
result = re.sub(
r"""href\s*=\s*("|')(/?[^:>]+/)\1""",
rf"""href=\1\2index.{lang}.html\1""",
result,
flags=re.MULTILINE | re.IGNORECASE,
)
return result

View File

@ -1,43 +0,0 @@
#!/usr/bin/env bash
inc_logging=true
logname() {
name="$1"
if [ -w "$statusdir" ] && touch "$statusdir/$name"; then
echo "$statusdir/$name"
elif echo "$forcedlog" | egrep -q "^${name}=.+"; then
echo "$forcedlog" |
sed -rn "s;^${name}=;;p"
else
echo /dev/null
fi
}
forcelog() {
name="$1"
[ "$(logname "$name")" = "/dev/null" ] &&
forcedlog="$forcedlog\n${name}=$(mktemp -t w3bldXXXXXXXXX --suffix $$)"
}
[ -z "$USER" ] && USER="$(whoami)"
trap "trap - 0 2 3 6 9 15; find \"${TMPDIR:-/tmp}/\" -maxdepth 1 -user \"$USER\" -name \"w3bld*$$\" -delete" 0 2 3 6 9 15
logstatus() {
# pipeline atom to write data streams into a log file
tee "$(logname "$1")"
}
t_logstatus() {
# pipeline atom to write data streams into a log file
while read line; do
printf "[$(date +%T)] %s\n" "$line"
done | logstatus "$@"
}
logappend() {
# pipeline atom to write data streams into a log file
tee -a "$(logname "$1")"
}

View File

@ -1,235 +0,0 @@
#!/usr/bin/env bash
inc_makerules=true
tree_maker() {
# walk through file tree and issue Make rules according to file type
input="$(realpath "$1")"
output="$(realpath "$2")"
cat <<EOF
# -----------------------------------------------------------------------------
# Makefile for FSFE website build, phase 2
# -----------------------------------------------------------------------------
.PHONY: all
.DELETE_ON_ERROR:
.SECONDEXPANSION:
PROCESSOR = "$basedir/build/process_file.sh"
PROCFLAGS = --build-env "${build_env}" --source "$basedir"
INPUTDIR = $input
OUTPUTDIR = $output
STATUSDIR = $statusdir
LANGUAGES = $languages
# -----------------------------------------------------------------------------
# Build .html files from .xhtml sources
# -----------------------------------------------------------------------------
# All .xhtml source files
HTML_SRC_FILES := \$(shell find "\$(INPUTDIR)" \
-name '*.??.xhtml' \
-not -path '\$(INPUTDIR)/.git/*' \
)
# All basenames of .xhtml source files (without .<lang>.xhtml ending)
# Note: \$(sort ...) is used to remove duplicates
HTML_SRC_BASES := \$(sort \$(basename \$(basename \$(HTML_SRC_FILES))))
# All directories containing .xhtml source files
HTML_SRC_DIRS := \$(sort \$(dir \$(HTML_SRC_BASES)))
# The same as above, but moved to the output directory
HTML_DST_BASES := \$(patsubst \$(INPUTDIR)/%,\$(OUTPUTDIR)/%,\$(HTML_SRC_BASES))
# List of .<lang>.html files to build
HTML_DST_FILES := \$(foreach base,\$(HTML_DST_BASES),\$(foreach lang,\$(LANGUAGES),\$(base).\$(lang).html))
# .xmllist file used to build a html file
XMLLIST_DEP = \$(wildcard \$(INPUTDIR)/\$(dir \$*).\$(notdir \$*).xmllist)
# .xsl file used to build a html file
XSL_DEP = \$(firstword \$(wildcard \$(INPUTDIR)/\$*.xsl) \$(INPUTDIR)/\$(dir \$*).default.xsl)
all: \$(HTML_DST_FILES)
EOF
for lang in ${languages}; do
cat <<EOF
\$(filter %.${lang}.html,\$(HTML_DST_FILES)): \$(OUTPUTDIR)/%.${lang}.html: \$(INPUTDIR)/%.*.xhtml \$\$(XMLLIST_DEP) \$\$(XSL_DEP) \$(INPUTDIR)/global/data/texts/.texts.${lang}.xml \$(INPUTDIR)/global/data/texts/texts.en.xml \$(INPUTDIR)/global/data/topbanner/.topbanner.${lang}.xml
echo "* Building \$*.${lang}.html"
\${PROCESSOR} \${PROCFLAGS} process_file "\$(INPUTDIR)/\$*.${lang}.xhtml" > "\$@"
EOF
done
cat <<EOF
# -----------------------------------------------------------------------------
# Create index.* symlinks
# -----------------------------------------------------------------------------
# All .xhtml source files with the same name as their parent directory
INDEX_SRC_FILES := \$(wildcard \$(foreach directory,\$(HTML_SRC_DIRS),\$(directory)\$(notdir \$(directory:/=)).??.xhtml))
# All basenames of .xhtml source files with the same name as their parent
# directory
INDEX_SRC_BASES := \$(sort \$(basename \$(basename \$(INDEX_SRC_FILES))))
# All directories containing .xhtml source files with the same name as their
# parent directory (that is, all directories in which index files should be
# created)
INDEX_SRC_DIRS := \$(dir \$(INDEX_SRC_BASES))
# The same as above, but moved to the output directory
INDEX_DST_DIRS := \$(patsubst \$(INPUTDIR)/%,\$(OUTPUTDIR)/%,\$(INDEX_SRC_DIRS))
# List of index.<lang>.html symlinks to create
INDEX_DST_LINKS := \$(foreach base,\$(INDEX_DST_DIRS),\$(foreach lang,\$(LANGUAGES),\$(base)index.\$(lang).html))
all: \$(INDEX_DST_LINKS)
EOF
for lang in ${languages}; do
cat <<EOF
\$(filter %/index.${lang}.html,\$(INDEX_DST_LINKS)): \$(OUTPUTDIR)/%/index.${lang}.html:
echo "* Creating symlink \$*/index.${lang}.html"
ln -sf "\$(notdir \$*).${lang}.html" "\$@"
EOF
done
cat <<EOF
# -----------------------------------------------------------------------------
# Create symlinks from file.<lang>.html to file.html.<lang>
# -----------------------------------------------------------------------------
# List of .html.<lang> symlinks to create
HTML_DST_LINKS := \$(foreach base,\$(HTML_DST_BASES) \$(addsuffix index,\$(INDEX_DST_DIRS)),\$(foreach lang,\$(LANGUAGES),\$(base).html.\$(lang)))
all: \$(HTML_DST_LINKS)
EOF
for lang in ${languages}; do
cat <<EOF
\$(OUTPUTDIR)/%.html.${lang}:
echo "* Creating symlink \$*.html.${lang}"
ln -sf "\$(notdir \$*).${lang}.html" "\$@"
EOF
done
cat <<EOF
# -----------------------------------------------------------------------------
# Build .rss files from .xhtml sources
# -----------------------------------------------------------------------------
# All .rss.xsl scripts which can create .rss output
RSS_SRC_SCRIPTS := \$(shell find "\$(INPUTDIR)" \
-name '*.rss.xsl' \
-not -path '\$(INPUTDIR)/.git/*' \
)
# All basenames of .xhtml source files from which .rss files should be built
RSS_SRC_BASES := \$(sort \$(basename \$(basename \$(RSS_SRC_SCRIPTS))))
# The same as above, but moved to the output directory
RSS_DST_BASES := \$(patsubst \$(INPUTDIR)/%,\$(OUTPUTDIR)/%,\$(RSS_SRC_BASES))
# List of .<lang>.rss files to build
RSS_DST_FILES := \$(foreach base,\$(RSS_DST_BASES),\$(foreach lang,\$(LANGUAGES),\$(base).\$(lang).rss))
all: \$(RSS_DST_FILES)
EOF
for lang in ${languages}; do
cat <<EOF
\$(OUTPUTDIR)/%.${lang}.rss: \$(INPUTDIR)/%.*.xhtml \$\$(XMLLIST_DEP) \$(INPUTDIR)/%.rss.xsl \$(INPUTDIR)/global/data/texts/.texts.${lang}.xml \$(INPUTDIR)/global/data/texts/texts.en.xml
echo "* Building \$*.${lang}.rss"
\${PROCESSOR} \${PROCFLAGS} process_file "\$(INPUTDIR)/\$*.${lang}.xhtml" "\$(INPUTDIR)/\$*.rss.xsl" > "\$@"
EOF
done
cat <<EOF
# -----------------------------------------------------------------------------
# Build .ics files from .xhtml sources
# -----------------------------------------------------------------------------
# All .ics.xsl scripts which can create .ics output
ICS_SRC_SCRIPTS := \$(shell find "\$(INPUTDIR)" \
-name '*.ics.xsl' \
-not -path '\$(INPUTDIR)/.git/*' \
)
# All basenames of .xhtml source files from which .ics files should be built
ICS_SRC_BASES := \$(sort \$(basename \$(basename \$(ICS_SRC_SCRIPTS))))
# The same as above, but moved to the output directory
ICS_DST_BASES := \$(patsubst \$(INPUTDIR)/%,\$(OUTPUTDIR)/%,\$(ICS_SRC_BASES))
# List of .<lang>.ics files to build
ICS_DST_FILES := \$(foreach base,\$(ICS_DST_BASES),\$(foreach lang,\$(LANGUAGES),\$(base).\$(lang).ics))
all: \$(ICS_DST_FILES)
EOF
for lang in ${languages}; do
cat <<EOF
\$(OUTPUTDIR)/%.${lang}.ics: \$(INPUTDIR)/%.*.xhtml \$\$(XMLLIST_DEP) \$(INPUTDIR)/%.ics.xsl \$(INPUTDIR)/global/data/texts/.texts.${lang}.xml \$(INPUTDIR)/global/data/texts/texts.en.xml
echo "* Building \$*.${lang}.ics"
\${PROCESSOR} \${PROCFLAGS} process_file "\$(INPUTDIR)/\$*.${lang}.xhtml" "\$(INPUTDIR)/\$*.ics.xsl" > "\$@"
EOF
done
cat <<EOF
# -----------------------------------------------------------------------------
# Copy images, docments etc
# -----------------------------------------------------------------------------
# All files which should just be copied over
COPY_SRC_FILES := \$(shell find -L "\$(INPUTDIR)" -type f \
-regex "\$(INPUTDIR)/[a-z\.]+\.[a-z]+/.*" \
-not -name '.drone.yml' \
-not -name '.gitignore' \
-not -name 'README*' \
-not -name 'Makefile' \
-not -name '*.sources' \
-not -name "*.xmllist" \
-not -name '*.xhtml' \
-not -name '*.xml' \
-not -name '*.xsl' \
-not -name '*.nix' \
) \$(INPUTDIR)/fsfe.org/order/data/items.en.xml
# The same as above, but moved to the output directory
COPY_DST_FILES := \$(sort \$(patsubst \$(INPUTDIR)/%,\$(OUTPUTDIR)/%,\$(COPY_SRC_FILES)))
all: \$(COPY_DST_FILES)
\$(COPY_DST_FILES): \$(OUTPUTDIR)/%: \$(INPUTDIR)/%
echo "* Copying file \$*"
rsync -l "\$<" "\$@"
# -----------------------------------------------------------------------------
# Clean up excess files in target directory
# -----------------------------------------------------------------------------
ALL_DST := \$(HTML_DST_FILES) \$(INDEX_DST_LINKS) \$(HTML_DST_LINKS) \$(RSS_DST_FILES) \$(ICS_DST_FILES) \$(COPY_DST_FILES) \$(SOURCE_DST_FILES)
.PHONY: clean
all: clean
clean:
# Write all destination filenames into "manifest" file, one per line
\$(file >\$(STATUSDIR)/manifest)
\$(foreach filename,\$(ALL_DST),\$(file >>\$(STATUSDIR)/manifest,\$(filename)))
sort "\$(STATUSDIR)/manifest" > "\$(STATUSDIR)/manifest.sorted"
find -L "\$(OUTPUTDIR)" -type f -path "\$(STATUSDIR)" -prune \\
| sort \\
| diff - "\$(STATUSDIR)/manifest.sorted" \\
| sed -rn 's;^< ;;p' \\
| while read file; do echo "* Deleting \$\${file}"; rm "\$\${file}"; done
# -----------------------------------------------------------------------------
EOF
}

View File

@ -1,23 +0,0 @@
#!/usr/bin/env bash
inc_misc=true
[ -z "$inc_logging" ] && . "$basedir/build/logging.sh"
debug() {
if [ "$#" -ge 1 ]; then
echo "$(date '+%F %T'): $@" | logappend debug >&2
else
logappend debug >&2
fi
}
print_error() {
echo "Error - $@" | logappend lasterror >&2
echo "Run '$0 --help' to see usage instructions" >&2
}
die() {
echo "$(date '+%F %T'): Fatal - $@" | logappend lasterror >&2
date +%s | logstatus end_time
exit 1
}

20
build/phase0/full.py Normal file
View File

@ -0,0 +1,20 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
from build.lib.misc import run_command
logger = logging.getLogger(__name__)
def full() -> None:
"""
Git clean the repo to remove all cached artifacts
Excluded the root .venv repo, as removing it mid build breaks the build, obviously
"""
logger.info("Performing a full rebuild, git cleaning")
run_command(
["git", "clean", "-fdx", "--exclude", "/.venv"],
)

6
build/phase1/__init__.py Normal file
View File

@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
# __init__.py is a special Python file that allows a directory to become
# a Python package so it can be accessed using the 'import' statement.

View File

@ -0,0 +1,62 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import csv
import logging
import os
import requests
from pathlib import Path
from urllib.parse import urlparse
logger = logging.getLogger(__name__)
raw_url = urlparse("https://git.fsfe.org/FSFE/activities/raw/branch/master/activities.csv")
def create_activities_file():
git_token = os.environ.get("GIT_TOKEN")
if git_token is None:
logger.warn("GIT_TOKEN is not set, skipping generation of activities file")
return
url = raw_url._replace(query=f"token={git_token}").geturl()
r = requests.get(url)
if not r.ok:
logger.error("Failed to retrieve activities file")
raise Exception("Failed to retrieve activities file")
activities_csv = csv.reader(r.text.split("\n")[1:], delimiter="\t")
activities = ""
for row in activities_csv:
if len(row) == 0:
continue
tag = row[0]
description = row[1]
event = row[2]
activities += f' <option value="{tag}||{description}"'
if event:
activities += f' data-event="{event}"'
activities += '>'
activities += f"{tag} ({description})"
activities += "</option>\n"
content = f"""<?xml version="1.0" encoding="UTF-8"?>
<data>
<version>1</version>
<module>
{activities}
</module>
</data>"""
activities_path = Path("global/data/modules/fsfe-activities-options.en.xml")
with open(activities_path, "w") as f:
f.write(content)
logger.info(f"Wrote activity file to {str(activities_path)}")

View File

@ -0,0 +1,39 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import multiprocessing
from itertools import product
from pathlib import Path
logger = logging.getLogger(__name__)
def _do_symlinking(type: str, lang: str) -> None:
"""
Helper function for doing all of the global symlinking that is suitable for multithreading
"""
target = (
Path(f"global/data/{type}/{type}.{lang}.xml")
if Path(f"global/data/{type}/{type}.{lang}.xml").exists()
else Path(f"global/data/{type}/{type}.en.xml")
)
source = Path(f"global/data/{type}/.{type}.{lang}.xml")
if not source.exists():
source.symlink_to(target.relative_to(source.parent))
def global_symlinks(languages: list[str], pool: multiprocessing.Pool) -> None:
"""
After this step, the following symlinks will exist:
* global/data/texts/.texts.<lang>.xml for each language
* global/data/topbanner/.topbanner.<lang>.xml for each language
Each of these symlinks will point to the corresponding file without a dot at
the beginning of the filename, if present, and to the English version
otherwise. This symlinks make sure that phase 2 can easily use the right file
for each language, also as a prerequisite in the Makefile.
"""
logger.info("Creating global symlinks")
types = ["texts", "topbanner"]
pool.starmap(_do_symlinking, product(types, languages))

View File

@ -0,0 +1,121 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
# Build an index for the search engine based on the article titles and tags
import json
import logging
import multiprocessing
from pathlib import Path
import iso639
import lxml.etree as etree
import nltk
from nltk.corpus import stopwords as nltk_stopwords
from build.lib.misc import update_if_changed
logger = logging.getLogger(__name__)
def _find_teaser(document: etree.ElementTree) -> str:
"""
Find a suitable teaser for indexation
Get all the paragraphs in <body> and return the first which contains more
than 10 words
:document: The parsed lxml ElementTree document
:returns: The text of the teaser or an empty string
"""
for p in document.xpath("//body//p"):
if p.text and len(p.text.strip().split(" ")) > 10:
return p.text
return ""
def _process_file(file: Path, stopwords: set[str]) -> dict:
"""
Generate the search index entry for a given file and set of stopwords
"""
logger.debug(f"Processing file {file}")
xslt_root = etree.parse(file)
tags = map(
lambda tag: tag.get("key"),
filter(lambda tag: tag.get("key") != "front-page", xslt_root.xpath("//tag")),
)
return {
"url": f"/{file.with_suffix('.html')}",
"tags": " ".join(tags),
"title": (
xslt_root.xpath("//html//title")[0].text
if xslt_root.xpath("//html//title")
else ""
),
"teaser": " ".join(
w
for w in _find_teaser(xslt_root).strip().split(" ")
if w.lower() not in stopwords
),
"type": "news" if "news/" in str(file) else "page",
# Get the date of the file if it has one
"date": (
xslt_root.xpath("//news[@newsdate]").get("newsdate")
if xslt_root.xpath("//news[@newsdate]")
else None
),
}
def index_websites(languages: list[str], pool: multiprocessing.Pool) -> None:
"""
Generate a search index for all sites that have a search/search.js file
"""
logger.info("Creating search indexes")
# Download all stopwords
nltkdir = "./.nltk_data"
nltk.data.path = [nltkdir] + nltk.data.path
nltk.download("stopwords", download_dir=nltkdir, quiet=True)
# Iterate over sites
for site in filter(
lambda path: path.joinpath("search/search.js").exists(),
Path(".").glob("?*.??*"),
):
logger.debug(f"Indexing {site}")
# Get all xhtml files in languages to be processed
# Create a list of tuples
# The first element of each tuple is the file and the second is a set of stopwords for that language
# Use iso639 to get the english name of the language from the two letter iso639-1 code we use to mark files.
# Then if that language has stopwords from nltk, use those stopwords.
files_with_stopwords = map(
lambda file: (
file,
(
set(
nltk_stopwords.words(
iso639.Language.from_part1(
file.suffixes[0].removeprefix(".")
).name.lower()
)
)
if iso639.Language.from_part1(
file.suffixes[0].removeprefix(".")
).name.lower()
in nltk_stopwords.fileids()
else set()
),
),
filter(
lambda file: file.suffixes[0].removeprefix(".") in languages,
Path(site).glob("**/*.??.xhtml"),
),
)
articles = pool.starmap(_process_file, files_with_stopwords)
update_if_changed(
Path(f"{site}/search/index.js"),
"var pages = " + json.dumps(articles, ensure_ascii=False),
)

View File

@ -0,0 +1,30 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import sys
from pathlib import Path
logger = logging.getLogger(__name__)
def prepare_subdirectories(languages: list[str], processes: int) -> None:
"""
Find any makefiles in subdirectories and run them
"""
logger.info("Preparing Subdirectories")
for subdir_path in map(
lambda path: path.parent, Path("").glob("?*.?*/**/subdir.py")
):
logger.info(f"Preparing subdirectory {subdir_path}")
sys.path.append(str(subdir_path.resolve()))
import subdir
subdir.run(languages, processes, subdir_path)
# Remove its path from where things can be imported
sys.path.remove(str(subdir_path.resolve()))
# Remove it from loaded modules
sys.modules.pop("subdir")
# prevent us from accessing it again
del subdir

128
build/phase1/run.py Normal file
View File

@ -0,0 +1,128 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
# -----------------------------------------------------------------------------
# script for FSFE website build, phase 1
# -----------------------------------------------------------------------------
# This script is executed in the root of the source directory tree, and
# creates some .xml and xhtml files as well as some symlinks, all of which
# serve as input files in phase 2. The whole phase 1 runs within the source
# directory tree and does not touch the target directory tree at all.
# -----------------------------------------------------------------------------
import logging
import multiprocessing
from .create_activities_file import create_activities_file
from .global_symlinks import global_symlinks
from .index_website import index_websites
from .prepare_subdirectories import prepare_subdirectories
from .update_css import update_css
from .update_defaultxsls import update_defaultxsls
from .update_localmenus import update_localmenus
from .update_stylesheets import update_stylesheets
from .update_tags import update_tags
from .update_xmllists import update_xmllists
logger = logging.getLogger(__name__)
def phase1_run(languages: list[str], processes: int, pool: multiprocessing.Pool):
"""
Run all the necessary sub functions for phase1.
"""
logger.info("Starting Phase 1 - Setup")
# -----------------------------------------------------------------------------
# Create XML activities file
# -----------------------------------------------------------------------------
create_activities_file()
# -----------------------------------------------------------------------------
# Build search index
# -----------------------------------------------------------------------------
# This step runs a Python tool that creates an index of all news and
# articles. It extracts titles, teaser, tags, dates and potentially more.
# The result will be fed into a JS file.
index_websites(languages, pool)
# -----------------------------------------------------------------------------
# Update CSS files
# -----------------------------------------------------------------------------
# This step recompiles the less files into the final CSS files to be
# distributed to the web server.
update_css()
# -----------------------------------------------------------------------------
# Update XSL stylesheets
# -----------------------------------------------------------------------------
# This step updates (actually: just touches) all XSL files which depend on
# another XSL file that has changed since the last build run. The phase 2
# Makefile then only has to consider the directly used stylesheet as a
# prerequisite for building each file and doesn't have to worry about other
# stylesheets imported into that one.
# This must run before the "dive into subdirectories" step, because in the news
# and events directories, the XSL files, if updated, will be copied for the
# per-year archives.
update_stylesheets(pool)
# -----------------------------------------------------------------------------
# Dive into subdirectories
# -----------------------------------------------------------------------------
# Find any makefiles in subdirectories and run them
prepare_subdirectories(languages, processes)
# -----------------------------------------------------------------------------
# Create XML symlinks
# -----------------------------------------------------------------------------
# After this step, the following symlinks will exist:
# * global/data/texts/.texts.<lang>.xml for each language
# * global/data/topbanner/.topbanner.<lang>.xml for each language
# Each of these symlinks will point to the corresponding file without a dot at
# the beginning of the filename, if present, and to the English version
# otherwise. This symlinks make sure that phase 2 can easily use the right file
# for each language, also as a prerequisite in the Makefile.
global_symlinks(languages, pool)
# -----------------------------------------------------------------------------
# Create XSL symlinks
# -----------------------------------------------------------------------------
# After this step, each directory with source files for HTML pages contains a
# symlink named .default.xsl and pointing to the default.xsl "responsible" for
# this directory. These symlinks make it easier for the phase 2 Makefile to
# determine which XSL script should be used to build a HTML page from a source
# file.
update_defaultxsls(pool)
# -----------------------------------------------------------------------------
# Update local menus
# -----------------------------------------------------------------------------
# After this step, all .localmenu.??.xml files will be up to date.
update_localmenus(languages, pool)
# -----------------------------------------------------------------------------
# Update tags
# -----------------------------------------------------------------------------
# After this step, the following files will be up to date:
# * tags/tagged-<tags>.en.xhtml for each tag used. Apart from being
# automatically created, these are regular source files for HTML pages, and
# in phase 2 are built into pages listing all news items and events for a
# tag.
# * tags/.tags.??.xml with a list of the tags used.
update_tags(languages, pool)
# -----------------------------------------------------------------------------
# Update XML filelists
# -----------------------------------------------------------------------------
# After this step, the following files will be up to date:
# * <dir>/.<base>.xmllist for each <dir>/<base>.sources as well as for each
# $site/tags/tagged-<tags>.en.xhtml. These files are used in phase 2 to include the
# correct XML files when generating the HTML pages. It is taken care that
# these files are only updated whenever their content actually changes, so
# they can serve as a prerequisite in the phase 2 Makefile.
update_xmllists(languages, pool)

View File

@ -0,0 +1,44 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
from pathlib import Path
import minify
from build.lib.misc import run_command, update_if_changed
logger = logging.getLogger(__name__)
def update_css() -> None:
"""
If any less files have been changed, update the css.
Compile less found at website/look/(fsfe.less|valentine.less)
Then minify it, and place it in the expected location for the build process.
"""
logger.info("Updating css")
for dir in Path("").glob("?*.?*/look"):
for name in ["fsfe", "valentine"]:
if dir.joinpath(name + ".less").exists() and (
not dir.joinpath(name + ".min.css").exists()
or any(
map(
lambda path: path.stat().st_mtime
> dir.joinpath(name + ".min.css").stat().st_mtime,
dir.glob("**/*.less"),
)
)
):
logger.info(f"Compiling {name}.less")
result = run_command(
[
"lessc",
str(dir.joinpath(name + ".less")),
],
)
update_if_changed(
dir.joinpath(name + ".min.css"),
minify.string("text/css", result),
)

View File

@ -0,0 +1,41 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import multiprocessing
from pathlib import Path
logger = logging.getLogger(__name__)
def _do_symlinking(directory: Path) -> None:
"""
In each dir, place a .default.xsl symlink pointing to the nearest default.xsl
"""
working_dir = directory
if not directory.joinpath(".default.xsl").exists():
while not working_dir.joinpath("default.xsl").exists():
working_dir = working_dir.parent
directory.joinpath(".default.xsl").symlink_to(
working_dir.joinpath("default.xsl").resolve()
)
def update_defaultxsls(pool: multiprocessing.Pool) -> None:
"""
Place a .default.xsl into each directory containing source files for
HTML pages (*.xhtml). These .default.xsl are symlinks to the first
available actual default.xsl found when climbing the directory tree
upwards, it's the xsl stylesheet to be used for building the HTML
files from this directory.
"""
logger.info("Updating default xsl's")
# Get a set of all directories containing .xhtml source files
directories = set(
map(lambda path: path.parent, Path(".").glob("*?.?*/**/*.*.xhtml"))
)
# Do all directories asynchronously
pool.map(_do_symlinking, directories)

118
build/phase1/update_localmenus.py Executable file
View File

@ -0,0 +1,118 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import multiprocessing
from pathlib import Path
import lxml.etree as etree
from build.lib.misc import get_basepath
logger = logging.getLogger(__name__)
def _write_localmenus(
dir: str, files_by_dir: dict[str, list[Path]], languages: list[str]
) -> None:
"""
Write localmenus for a given directory
"""
# Set of files with no langcode or xhtml extension
base_files = set(
map(
lambda filter_file: get_basepath(filter_file),
files_by_dir[dir],
)
)
for lang in languages:
file = Path(dir).joinpath(f".localmenu.{lang}.xml")
logger.debug(f"Creating {file}")
page = etree.Element("feed")
# Add the subelements
version = etree.SubElement(page, "version")
version.text = "1"
for source_file in filter(
lambda path: path is not None,
map(
lambda base_file: base_file.with_suffix(f".{lang}.xhtml")
if base_file.with_suffix(f".{lang}.xhtml").exists()
else (
base_file.with_suffix(".en.xhtml")
if base_file.with_suffix(".en.xhtml").exists()
else None
),
base_files,
),
):
for localmenu in etree.parse(source_file).xpath("//localmenu"):
etree.SubElement(
page,
"localmenuitem",
set=(
str(localmenu.xpath("./@set")[0])
if localmenu.xpath("./@set") != []
else "default"
),
id=(
str(localmenu.xpath("./@id")[0])
if localmenu.xpath("./@id") != []
else "default"
),
link=(
str(
source_file.with_suffix(".html").relative_to(
source_file.parents[0]
)
)
),
).text = localmenu.text
page.getroottree().write(file, xml_declaration=True, encoding="utf-8")
def update_localmenus(languages: list[str], pool: multiprocessing.Pool) -> None:
"""
Update all the .localmenu.*.xml files containing the local menus.
"""
logger.info("Updating local menus")
# Get a dict of all source files containing local menus
files_by_dir = {}
for file in filter(
lambda path: "-template" not in str(path),
Path(".").glob("*?.?*/**/*.??.xhtml"),
):
xslt_root = etree.parse(file)
if xslt_root.xpath("//localmenu"):
dir = xslt_root.xpath("//localmenu/@dir")
dir = dir[0] if dir else str(file.parent.relative_to(Path(".")))
if dir not in files_by_dir:
files_by_dir[dir] = set()
files_by_dir[dir].add(file)
for dir in files_by_dir:
files_by_dir[dir] = sorted(list(files_by_dir[dir]))
# If any of the source files has been updated, rebuild all .localmenu.*.xml
dirs = filter(
lambda dir: (
any(
map(
lambda file: (
(not Path(dir).joinpath(".localmenu.en.xml").exists())
or (
file.stat().st_mtime
> Path(dir).joinpath(".localmenu.en.xml").stat().st_mtime
)
),
files_by_dir[dir],
)
)
),
files_by_dir,
)
pool.starmap(
_write_localmenus, map(lambda dir: (dir, files_by_dir, languages), dirs)
)

View File

@ -0,0 +1,49 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import multiprocessing
import re
from pathlib import Path
from lxml import etree
from build.lib.misc import touch_if_newer_dep
logger = logging.getLogger(__name__)
def _update_sheet(file: Path) -> None:
"""
Update a given xsl file if any of its dependant xsl files have been updated
"""
xslt_root = etree.parse(file)
imports = map(
lambda imp: file.parent.joinpath(imp.get("href"))
.resolve()
.relative_to(Path(".").resolve()),
xslt_root.xpath(
"//xsl:import", namespaces={"xsl": "http://www.w3.org/1999/XSL/Transform"}
),
)
touch_if_newer_dep(file, imports)
def update_stylesheets(pool: multiprocessing.Pool) -> None:
"""
This script is called from the phase 1 Makefile and touches all XSL files
which depend on another XSL file that has changed since the last build run.
The phase 2 Makefile then only has to consider the
directly used stylesheet as a prerequisite for building each file and doesn't
have to worry about other stylesheets imported into that one.
"""
logger.info("Updating XSL stylesheets")
banned = re.compile(r"(\.venv/.*)|(.*\.default\.xsl$)")
pool.map(
_update_sheet,
filter(
lambda file: re.match(banned, str(file)) is None,
Path(".").glob("**/*.xsl"),
),
)

179
build/phase1/update_tags.py Executable file
View File

@ -0,0 +1,179 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import multiprocessing
from pathlib import Path
from xml.sax.saxutils import escape
import lxml.etree as etree
from build.lib.misc import (
get_basepath,
keys_exists,
lang_from_filename,
sort_dict,
update_if_changed,
)
logger = logging.getLogger(__name__)
def _update_tag_pages(site: Path, tag: str, languages: list[str]) -> None:
"""
Update the xhtml pages and xmllists for a given tag
"""
for lang in languages:
tagfile_source = Path(f"{site}/tags/tagged.{lang}.xhtml")
if tagfile_source.exists():
taggedfile = Path(f"{site}/tags/tagged-{tag}.{lang}.xhtml")
content = tagfile_source.read_text().replace("XXX_TAGNAME_XXX", tag)
update_if_changed(taggedfile, content)
def _update_tag_sets(
site: Path,
lang: str,
filecount: dict[str, dict[str, int]],
files_by_tag: dict[str, list[Path]],
tags_by_lang: dict[str, dict[str, str]],
) -> None:
"""
Update the .tags.??.xml tagset xmls for a given tag
"""
# Add uout toplevel element
page = etree.Element("tagset")
# Add the subelements
version = etree.SubElement(page, "version")
version.text = "1"
for section in ["news", "events"]:
for tag in files_by_tag:
count = filecount[section][tag]
label = (
tags_by_lang[lang][tag]
if keys_exists(tags_by_lang, lang, tag)
and tags_by_lang[lang][tag] is not None
else tags_by_lang["en"][tag]
if keys_exists(tags_by_lang, "en", tag)
and tags_by_lang["en"][tag] is not None
else tag
)
if count > 0:
etree.SubElement(
page, "tag", section=section, key=tag, count=str(count)
).text = label
update_if_changed(
Path(f"{site}/tags/.tags.{lang}.xml"),
etree.tostring(page, encoding="utf-8").decode("utf-8"),
)
def update_tags(languages: list[str], pool: multiprocessing.Pool) -> None:
"""
Update Tag pages, xmllists and xmls
Creates/update the following files:
* */tags/tagged-<tags>.en.xhtml for each tag used. Apart from being
automatically created, these are regular source files for HTML pages, and
in phase 2 are built into pages listing all news items and events for a
tag.
* */tags/.tags.??.xml with a list of the tags used.
Changing or removing tags in XML files is also considered, in which case a
file is removed from the .xmllist files.
When a tag has been removed from the last XML file where it has been used,
the tagged-* are correctly deleted.
"""
for site in filter(
lambda path: path.joinpath("tags").exists(),
Path(".").glob("?*.??*"),
):
logger.info(f"Updating tags for {site}")
# Create a complete and current map of which tag is used in which files
files_by_tag = {}
tags_by_lang = {}
# Fill out files_by_tag and tags_by_lang
for file in filter(
lambda file:
# Not in tags dir of a site
site.joinpath("tags") not in file.parents,
site.glob("**/*.xml"),
):
for tag in etree.parse(file).xpath("//tag"):
# Get the key attribute, and filter out some invalid chars
key = (
tag.get("key")
.replace("/", "-")
.replace(" ", "-")
.replace(":", "-")
.strip()
)
# Get the label, and strip it.
label = (
escape(tag.text.strip()) if tag.text and tag.text.strip() else None
)
# Load into the dicts
if key not in files_by_tag:
files_by_tag[key] = set()
files_by_tag[key].add(get_basepath(file))
lang = lang_from_filename(file)
if lang not in tags_by_lang:
tags_by_lang[lang] = {}
tags_by_lang[lang][key] = (
tags_by_lang[lang][key]
if key in tags_by_lang[lang] and tags_by_lang[lang][key]
else label
)
# Sort dicts to ensure that they are stable between runs
files_by_tag = sort_dict(files_by_tag)
for tag in files_by_tag:
files_by_tag[tag] = sorted(files_by_tag[tag])
tags_by_lang = sort_dict(tags_by_lang)
for lang in tags_by_lang:
tags_by_lang[lang] = sort_dict(tags_by_lang[lang])
logger.debug("Updating tag pages")
pool.starmap(
_update_tag_pages,
map(lambda tag: (site, tag, languages), files_by_tag.keys()),
)
logger.debug("Updating tag lists")
pool.starmap(
update_if_changed,
map(
lambda tag: (
Path(f"{site}/tags/.tagged-{tag}.xmllist"),
("\n".join(map(lambda file: str(file), files_by_tag[tag])) + "\n"),
),
files_by_tag.keys(),
),
)
logger.debug("Updating tag sets")
# Get count of files with each tag in each section
filecount = {}
for section in ["news", "events"]:
filecount[section] = {}
for tag in files_by_tag:
filecount[section][tag] = len(
list(
filter(
lambda path: section in str(path.parent),
files_by_tag[tag],
)
)
)
pool.starmap(
_update_tag_sets,
map(
lambda lang: (site, lang, filecount, files_by_tag, tags_by_lang),
filter(lambda lang: lang in languages, tags_by_lang.keys()),
),
)

164
build/phase1/update_xmllists.py Executable file
View File

@ -0,0 +1,164 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import datetime
import fnmatch
import logging
import multiprocessing
import re
from pathlib import Path
import lxml.etree as etree
from build.lib.misc import (
get_basepath,
lang_from_filename,
touch_if_newer_dep,
update_if_changed,
)
logger = logging.getLogger(__name__)
def _update_for_base(
base: Path, all_xml: set[Path], nextyear: str, thisyear: str, lastyear: str
) -> None:
"""
Update the xmllist for a given base file
"""
matching_files = set()
# If sources exist
if base.with_suffix(".sources").exists():
# Load every file that matches the pattern
# If a tag is included in the pattern, the file must contain that tag
with base.with_suffix(".sources").open(mode="r") as file:
for line in file:
pattern = (
re.sub(r":\[.*\]$", "*", line)
.replace("$nextyear", nextyear)
.replace("$thisyear", thisyear)
.replace("$lastyear", lastyear)
.strip()
)
if len(pattern) <= 0:
logger.debug("Pattern too short, continue!")
continue
tag = (
re.match(r":\[(.*)\]$", line).group().strip()
if re.match(r":\[(.*)\]$", line)
else ""
)
for line in filter(
lambda line:
# Matches glob pattern
fnmatch.fnmatchcase(str(line), pattern)
# contains tag if tag in pattern
and (
etree.parse(file).find(f"//tag[@key='{tag}']")
if tag != ""
else True
)
# Not just matching an empty line
and len(str(line)) > 0,
all_xml,
):
matching_files.add(str(line))
for file in Path("").glob(f"{base}.??.xhtml"):
xslt_root = etree.parse(file)
for module in xslt_root.xpath("//module"):
matching_files.add(f"global/data/modules/{module.get('id')}".strip())
matching_files = sorted(matching_files)
update_if_changed(
Path(f"{base.parent}/.{base.name}.xmllist"),
("\n".join(matching_files) + "\n") if matching_files else "",
)
def _update_module_xmllists(languages: list[str], pool: multiprocessing.Pool) -> None:
"""
Update .xmllist files for .sources and .xhtml containing <module>s
"""
logger.info("Updating XML lists")
# Store current dir
for site in filter(lambda path: path.is_dir(), Path(".").glob("?*.??*")):
logger.info(f"Updating xmllists for {site}")
# Get all the bases and stuff before multithreading the update bit
all_xml = set(
map(
lambda path: get_basepath(path),
filter(
lambda path: lang_from_filename(path) in languages,
site.glob("**/*.*.xml"),
),
)
)
source_bases = set(
map(
lambda path: path.with_suffix(""),
site.glob("**/*.sources"),
)
)
module_bases = set(
map(
lambda path: get_basepath(path),
filter(
lambda path: lang_from_filename(path) in languages
and etree.parse(path).xpath("//module"),
site.glob("**/*.*.xhtml"),
),
)
)
all_bases = source_bases | module_bases
nextyear = str(datetime.datetime.today().year + 1)
thisyear = str(datetime.datetime.today().year)
lastyear = str(datetime.datetime.today().year - 1)
pool.starmap(
_update_for_base,
map(lambda base: (base, all_xml, nextyear, thisyear, lastyear), all_bases),
)
def _check_xmllist_deps(file: Path) -> None:
"""
If any of the sources in an xmllist are newer than it, touch the xmllist
"""
xmls = set()
with file.open(mode="r") as fileobj:
for line in fileobj:
for newfile in Path("").glob(line + ".??.xml"):
xmls.add(newfile)
touch_if_newer_dep(file, list(xmls))
def _touch_xmllists_with_updated_deps(
languages: list[str], pool: multiprocessing.Pool
) -> None:
"""
Touch all .xmllist files where one of the contained files has changed
"""
logger.info("Checking contents of XML lists")
pool.map(_check_xmllist_deps, Path("").glob("./**/.*.xmllist"))
def update_xmllists(languages: list[str], pool: multiprocessing.Pool) -> None:
"""
Update XML filelists (*.xmllist)
Creates/update the following files:
* <dir>/.<base>.xmllist for each <dir>/<base>.sources as well as for each
fsfe.org/tags/tagged-<tags>.en.xhtml. These files are used in phase 2 to include the
correct XML files when generating the HTML pages. It is taken care that
these files are only updated whenever their content actually changes, so
they can serve as a prerequisite in the phase 2 Makefile.
Changing or removing tags in XML files is also considered, in which case a
file is removed from the .xmllist files.
When a tag has been removed from the last XML file where it has been used,
the tagged-* are correctly deleted.
"""
_update_module_xmllists(languages, pool)
_touch_xmllists_with_updated_deps(languages, pool)

6
build/phase2/__init__.py Normal file
View File

@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
# __init__.py is a special Python file that allows a directory to become
# a Python package so it can be accessed using the 'import' statement.

View File

@ -0,0 +1,56 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import multiprocessing
from pathlib import Path
logger = logging.getLogger(__name__)
def _copy_file(target: Path, source_file: Path) -> None:
target_file = target.joinpath(source_file)
if (
not target_file.exists()
or source_file.stat().st_mtime > target_file.stat().st_mtime
):
logger.debug(f"Copying {source_file} to {target_file}")
target_file.parent.mkdir(parents=True, exist_ok=True)
target_file.write_bytes(source_file.read_bytes())
def copy_files(pool: multiprocessing.Pool, target: Path) -> None:
"""
Copy images, docments etc
"""
logger.info("Copying over media and misc files")
pool.starmap(
_copy_file,
map(
lambda file: (target, file),
list(
filter(
lambda path: path.is_file()
and path.suffix
not in [
".md",
".yml",
".gitignore",
".sources",
".xmllist",
".xhtml",
".xsl",
".xml",
".less",
".py",
".pyc",
]
and path.name not in ["Makefile"],
Path("").glob("*?.?*/**/*"),
)
)
# Special case hard code pass over orde items xml required by cgi script
+ list(Path("").glob("*?.?*/order/data/items.en.xml")),
),
)

View File

@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import multiprocessing
from pathlib import Path
from build.lib.misc import get_basename
logger = logging.getLogger(__name__)
def _do_symlinking(target: Path) -> None:
source = target.parent.joinpath(
f"index{target.with_suffix('').suffix}{target.suffix}"
)
if not source.exists():
source.symlink_to(target.relative_to(source.parent))
def create_index_symlinks(pool: multiprocessing.Pool, target: Path) -> None:
"""
Create index.* symlinks
"""
logger.info("Creating index symlinks")
pool.map(
_do_symlinking,
filter(
lambda path: get_basename(path) == path.parent.name,
target.glob("**/*.??.html"),
),
)

View File

@ -0,0 +1,26 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import multiprocessing
from pathlib import Path
logger = logging.getLogger(__name__)
def _do_symlinking(target: Path) -> None:
source = target.with_suffix("").with_suffix(f".html{target.with_suffix('').suffix}")
if not source.exists():
source.symlink_to(target.relative_to(source.parent))
def create_language_symlinks(pool: multiprocessing.Pool, target: Path) -> None:
"""
Create symlinks from file.<lang>.html to file.html.<lang>
"""
logger.info("Creating language symlinks")
pool.map(
_do_symlinking,
target.glob("**/*.??.html"),
)

View File

@ -0,0 +1,68 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import multiprocessing
from pathlib import Path
from build.lib.misc import get_basepath
from build.lib.process_file import process_file
logger = logging.getLogger(__name__)
def _process_stylesheet(languages: list[str], target: Path, source_xsl: Path) -> None:
base_file = get_basepath(source_xsl)
destination_base = target.joinpath(base_file)
for lang in languages:
target_file = destination_base.with_suffix(
f".{lang}{source_xsl.with_suffix('').suffix}"
)
source_xhtml = base_file.with_suffix(f".{lang}.xhtml")
if not target_file.exists() or any(
# If any source file is newer than the file to be generated
map(
lambda file: (
file.exists() and file.stat().st_mtime > target_file.stat().st_mtime
),
[
(
source_xhtml
if source_xhtml.exists()
else base_file.with_suffix(".en.xhtml")
),
source_xsl,
Path(f"global/data/texts/.texts.{lang}.xml"),
Path("global/data/texts/texts.en.xml"),
],
)
):
logger.debug(f"Building {target_file}")
result = process_file(source_xhtml, source_xsl)
target_file.parent.mkdir(parents=True, exist_ok=True)
target_file.write_text(result)
def process_rss_ics_files(
languages: list[str], pool: multiprocessing.Pool, target: Path
) -> None:
"""
Build .rss files from .xhtml sources
"""
logger.info("Processing rss files")
pool.starmap(
_process_stylesheet,
map(
lambda source_xsl: (languages, target, source_xsl),
Path("").glob("*?.?*/**/*.rss.xsl"),
),
)
logger.info("Processing ics files")
pool.starmap(
_process_stylesheet,
map(
lambda source_xsl: (languages, target, source_xsl),
Path("").glob("*?.?*/**/*.ics.xsl"),
),
)

View File

@ -0,0 +1,69 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import multiprocessing
from pathlib import Path
from build.lib.process_file import process_file
logger = logging.getLogger(__name__)
def _process_dir(languages: list[str], target: Path, dir: Path) -> None:
for basename in set(map(lambda path: path.with_suffix(""), dir.glob("*.??.xhtml"))):
for lang in languages:
source_file = basename.with_suffix(f".{lang}.xhtml")
target_file = target.joinpath(source_file).with_suffix(".html")
processor = (
basename.with_suffix(".xsl")
if basename.with_suffix(".xsl").exists()
else basename.parent.joinpath(".default.xsl")
)
if not target_file.exists() or any(
# If any source file is newer than the file to be generated
# If the file does not exist to
map(
lambda file: (
file.exists()
and file.stat().st_mtime > target_file.stat().st_mtime
),
[
(
source_file
if source_file.exists()
else basename.with_suffix(".en.xhtml")
),
processor,
target_file.with_suffix("").with_suffix(".xmllist"),
Path(f"global/data/texts/.texts.{lang}.xml"),
Path(f"global/data/topbanner/.topbanner.{lang}.xml"),
Path("global/data/texts/texts.en.xml"),
],
)
):
logger.debug(f"Building {target_file}")
result = process_file(source_file, processor)
target_file.parent.mkdir(parents=True, exist_ok=True)
target_file.write_text(result)
def process_xhtml_files(
languages: list[str], pool: multiprocessing.Pool, target: Path
) -> None:
"""
Build .html files from .xhtml sources
"""
# TODO
# It should be possible to upgrade this and process_rss_ics files such that only one functions is needed
# Also for performance it would be better to iterate by processor xls, and parse it only once and pass the xsl object to called function.
logger.info("Processing xhtml files")
pool.starmap(
_process_dir,
map(
lambda dir: (languages, target, dir),
set(map(lambda path: path.parent, Path("").glob("*?.?*/**/*.*.xhtml"))),
),
)

30
build/phase2/run.py Normal file
View File

@ -0,0 +1,30 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
# -----------------------------------------------------------------------------
# script for FSFE website build, phase 2
# -----------------------------------------------------------------------------
import logging
import multiprocessing
from pathlib import Path
from .copy_files import copy_files
from .create_index_symlinks import create_index_symlinks
from .create_language_symlinks import create_language_symlinks
from .process_rss_ics_files import process_rss_ics_files
from .process_xhtml_files import process_xhtml_files
logger = logging.getLogger(__name__)
def phase2_run(languages: list[str], pool: multiprocessing.Pool, target: Path):
"""
Run all the necessary sub functions for phase2.
"""
logger.info("Starting Phase 2 - Generating output")
process_xhtml_files(languages, pool, target)
create_index_symlinks(pool, target)
create_language_symlinks(pool, target)
process_rss_ics_files(languages, pool, target)
copy_files(pool, target)

6
build/phase3/__init__.py Normal file
View File

@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
# __init__.py is a special Python file that allows a directory to become
# a Python package so it can be accessed using the 'import' statement.

39
build/phase3/serve_websites.py Executable file
View File

@ -0,0 +1,39 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import http.server
import logging
import multiprocessing
import os
import socketserver
from pathlib import Path
logger = logging.getLogger(__name__)
def _run_webserver(path: str, port: int) -> None:
"""
Given a path as a string and a port it will
serve that dir on that localhost:port for forever.
"""
os.chdir(path)
Handler = http.server.CGIHTTPRequestHandler
with socketserver.TCPServer(("", port), Handler) as httpd:
httpd.serve_forever()
def serve_websites(serve_dir: str, base_port: int, increment_number: int) -> None:
"""
Takes a target directory, a base port and a number to increment port by per dir
It then serves all directories over http on localhost
"""
dirs = sorted(list(filter(lambda path: path.is_dir(), Path(serve_dir).iterdir())))
serves = []
for dir in dirs:
port = base_port + (increment_number * dirs.index(dir))
logging.info(f"{dir.name} served at http://127.0.0.1:{port}")
serves.append((str(dir), port))
with multiprocessing.Pool(len(serves)) as pool:
pool.starmap(_run_webserver, serves)

View File

@ -0,0 +1,49 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import multiprocessing
from pathlib import Path
from build.lib.misc import run_command
logger = logging.getLogger(__name__)
def _rsync(stagedir: Path, target: str, port: int) -> None:
run_command(
[
"rsync",
"-av",
"--copy-unsafe-links",
"--del",
str(stagedir) + "/",
target,
]
# Use ssh with a command such that it does not worry about fingerprints, as every connection is a new one basically
# Also specify the sshport, and only load this sshconfig if required
+ (
["-e", f"ssh -o StrictHostKeyChecking=accept-new -p {port}"]
if ":" in target
else []
),
)
def stage_to_target(stagedir: Path, targets: str, pool: multiprocessing.Pool) -> None:
"""
Use a multithreaded rsync to copy the stage dir to all targets.
"""
logger.info("Rsyncing from stage dir to target dir(s)")
pool.starmap(
_rsync,
map(
lambda target: (
stagedir,
(target if "?" not in target else target.split("?")[0]),
(int(target.split("?")[1]) if "?" in target else 22),
),
targets.split(","),
),
)

View File

@ -1,11 +0,0 @@
#!/usr/bin/env bash
basedir="${0%/*}/.."
[ -z "$inc_processor" ] && . "$basedir/build/processor.sh"
. "$basedir/build/arguments.sh"
case "$command" in
process_file) process_file "$workfile" "$processor" ;;
*) die "Unrecognised command or no command given" ;;
esac

View File

@ -1,56 +0,0 @@
#!/usr/bin/env bash
inc_processor=true
[ -z "$inc_filenames" ] && . "$basedir/build/filenames.sh"
[ -z "$inc_scaffold" ] && . "$basedir/build/scaffold.sh"
process_file() {
infile="$1"
processor="$2"
shortname=$(get_shortname "$infile")
lang=$(get_language "$infile")
if [ -z "${processor}" ]; then
if [ -f "${shortname}.xsl" ]; then
processor="${shortname}.xsl"
else
# Actually use the symlink target, so the relative includes are searched
# in the correct directory.
processor="$(realpath "${shortname%/*}/.default.xsl")"
fi
fi
# Make sure that the following pipe exits with a nonzero exit code if *any*
# of the commands fails.
set -o pipefail
# The sed command of death below does the following:
# 1. Remove https://fsfe.org (or https://test.fsfe.org) from the start of all
# links
# 2. Change links from /foo/bar.html into /foo/bar.xx.html
# 3. Change links from foo/bar.html into foo/bar.xx.html
# 4. Same for .rss and .ics links
# 5. Change links from /foo/bar/ into /foo/bar/index.xx.html
# 6. Change links from foo/bar/ into foo/bar/index.xx.html
# ... where xx is the language code.
# Everything is duplicated to allow for the href attribute to be enclosed in
# single or double quotes.
# I am strongly convinced that there must be a less obfuscated way of doing
# this. --Reinhard
build_xmlstream "$shortname" "$lang" |
xsltproc --stringparam "build-env" "${build_env}" "$processor" - |
sed -r ':X; N; $!bX;
s;<[\r\n\t ]*(a|link)([\r\n\t ][^>]*)?[\r\n\t ]href="((https?:)?//[^"]*)";<\1\2 href="#== norewrite ==\3";gI
s;<[\r\n\t ]*(a|link)([\r\n\t ][^>]*)?[\r\n\t ]href="([^#"])([^"]*/)?([^\./"]*\.)(html|rss|ics)(#[^"]*)?";<\1\2 href="\3\4\5'"$lang"'.\6\7";gI
s;<[\r\n\t ]*(a|link)([\r\n\t ][^>]*)?[\r\n\t ]href="([^#"]*/)(#[^"]*)?";<\1\2 href="\3index.'"$lang"'.html\4";gI
s;<[\r\n\t ]*(a|link)([\r\n\t ][^>]*)?[\r\n\t ]href="#== norewrite ==((https?:)?//[^"]*)";<\1\2 href="\3";gI
s;<[\r\n\t ]*(a|link)([\r\n\t ][^>]*)?[\r\n\t ]href='\''((https?:)?//[^'\'']*)'\'';<\1\2 href='\''#== norewrite ==\3'\'';gI
s;<[\r\n\t ]*(a|link)([\r\n\t ][^>]*)?[\r\n\t ]href='\''([^#'\''])([^'\'']*/)?([^\./'\'']*\.)(html|rss|ics)(#[^'\'']*)?'\'';<\1\2 href='\''\3\4\5'"$lang"'.\6\7'\'';gI
s;<[\r\n\t ]*(a|link)([\r\n\t ][^>]*)?[\r\n\t ]href='\''([^#'\'']*/)(#[^'\'']*)?'\'';<\1\2 href='\''\3index.'"$lang"'.html\4'\'';gI
s;<[\r\n\t ]*(a|link)([\r\n\t ][^>]*)?[\r\n\t ]href='\''#== norewrite ==((https?:)?//[^'\'']*)'\'';<\1\2 href='\''\3'\'';gI
'
}

View File

@ -1,125 +0,0 @@
#!/usr/bin/env bash
inc_scaffold=true
get_version() {
version=$(xsltproc $basedir/build/xslt/get_version.xsl $1)
echo ${version:-0}
}
include_xml() {
# include second level elements of a given XML file
# this emulates the behaviour of the original
# build script which wasn't able to load top
# level elements from any file
if [ -f "$1" ]; then
# Remove <version> because the filename attribute would otherwise be added
# to this element instead of the actual content element.
sed 's;<version>.*</version>;;' "$1" |
sed -r ':X; $bY; N; bX; :Y;
s:<(\?[xX][mM][lL]|!DOCTYPE)[[:space:]]+[^>]+>::g
s:<[^!][^>]*>::;
s:</[^>]*>([^<]*((<[^>]+/>|<!([^>]|<[^>]*>)*>|<\?[^>]+>)[^<]*)*)?$:\1:;'
fi
}
get_attributes() {
# get attributes of top level element in a given
# XHTML file
sed -rn ':X; N; $!bX;
s;^.*<[\n\t\r ]*([xX]|[xX]?[hH][tT])[mM][lL][\n\t\r ]+([^>]*)>.*$;\2;p' "$1"
}
list_langs() {
# list all languages a file exists in by globbing up
# the shortname (i.e. file path with file ending omitted)
# output is readily formatted for inclusion
# in xml stream
for file in "${1}".[a-z][a-z].xhtml; do
language="${file: -8:2}"
text="$(echo -n $(cat "${basedir}/global/languages/${language}"))"
echo "<tr id=\"${language}\">${text}</tr>"
done
}
auto_sources() {
# import elements from source files, add file name
# attribute to first element included from each file
shortname="$1"
lang="$2"
list_file="$(dirname ${shortname})/.$(basename ${shortname}).xmllist"
if [ -f "${list_file}" ]; then
cat "${list_file}" | while read path; do
base="$(basename ${path})"
if [ -f "${basedir}/${path}.${lang}.xml" ]; then
printf '\n### filename="%s" ###\n%s' "${base#.}" "$(include_xml "${basedir}/${path}.${lang}.xml")"
elif [ -f "${basedir}/${path}.en.xml" ]; then
printf '\n### filename="%s" ###\n%s' "${base#.}" "$(include_xml "${basedir}/${path}.en.xml")"
fi
done |
sed -r ':X; N; $!bX;
s;\n### (filename="[^\n"]+") ###\n[^<]*(<![^>]+>[^<]*)*(<([^/>]+/)*([^/>]+))(/?>);\2\3 \1\6;g;'
fi
}
build_xmlstream() {
# assemble the xml stream for feeding into xsltproc
# the expected shortname and language flag indicate
# a single xhtml page to be built
shortname="$1"
lang="$2"
olang="$(echo "${shortname}".[a-z][a-z].xhtml "${shortname}".[e]n.xhtml | sed -rn 's;^.*\.([a-z]{2})\.xhtml.*$;\1;p')"
dirname="${shortname%/*}/"
topbanner_xml="$basedir/global/data/topbanner/.topbanner.${lang}.xml"
texts_xml="$basedir/global/data/texts/.texts.${lang}.xml"
date="$(date +%Y-%m-%d)"
time="$(date +%H:%M:%S)"
if [ -f "${shortname}.${lang}.xhtml" ]; then
act_lang="$lang"
translation_state="up-to-date"
[ $(get_version "${shortname}.${olang}.xhtml") -gt $(get_version "${shortname}.${lang}.xhtml") ] && translation_state="outdated"
[ $(($(get_version "${shortname}.${olang}.xhtml") - 3)) -gt $(get_version "${shortname}.${lang}.xhtml") ] && act_lang="$olang" && translation_state="very-outdated"
else
act_lang="$olang"
translation_state="untranslated"
fi
infile="${shortname}.${act_lang}.xhtml"
cat <<-EOF
<buildinfo
date="$date"
original="$olang"
filename="/${shortname#"$basedir"/}"
fileurl="/${shortname#"$basedir"/*/}"
dirname="/${dirname#"$basedir"/}"
language="$lang"
translation_state="$translation_state"
>
<trlist>
$(list_langs "$shortname")
</trlist>
<topbanner>$(include_xml "$topbanner_xml")</topbanner>
<textsetbackup>$(include_xml "$basedir/global/data/texts/texts.en.xml")</textsetbackup>
<textset>$(include_xml "$texts_xml")</textset>
<document
language="$act_lang"
$(get_attributes "$infile")
>
<set>
$(auto_sources "${shortname}" "$lang")
</set>
$(include_xml "$infile")
</document>
</buildinfo>
EOF
}

View File

@ -5,12 +5,6 @@
<xsl:template name="body_scripts">
<script src="{$urlprefix}/scripts/bootstrap-3.0.3.custom.js"></script>
<xsl:if test="$build-env = 'development'">
<xsl:element name="script">
<xsl:attribute name="src"><xsl:value-of select="$urlprefix"/>/scripts/less.min.js</xsl:attribute>
</xsl:element>
</xsl:if>
</xsl:template>
</xsl:stylesheet>

View File

@ -54,45 +54,21 @@
</xsl:element>
<xsl:choose>
<xsl:when test="$build-env = 'development' and not(/buildinfo/document/@external)">
<xsl:choose>
<xsl:when test="$mode = 'valentine'">
<xsl:element name="link">
<xsl:attribute name="rel">stylesheet/less</xsl:attribute>
<xsl:attribute name="media">all</xsl:attribute>
<xsl:attribute name="href"><xsl:value-of select="$urlprefix"/>/look/valentine.less</xsl:attribute>
<xsl:attribute name="type">text/css</xsl:attribute>
</xsl:element>
</xsl:when>
<xsl:otherwise><!-- not valentine -->
<xsl:element name="link">
<xsl:attribute name="rel">stylesheet/less</xsl:attribute>
<xsl:attribute name="media">all</xsl:attribute>
<xsl:attribute name="href"><xsl:value-of select="$urlprefix"/>/look/fsfe.less</xsl:attribute>
<xsl:attribute name="type">text/css</xsl:attribute>
</xsl:element>
</xsl:otherwise>
</xsl:choose>
<xsl:when test="$mode = 'valentine'">
<xsl:element name="link">
<xsl:attribute name="rel">stylesheet</xsl:attribute>
<xsl:attribute name="media">all</xsl:attribute>
<xsl:attribute name="href"><xsl:value-of select="$urlprefix"/>/look/valentine.min.css</xsl:attribute>
<xsl:attribute name="type">text/css</xsl:attribute>
</xsl:element>
</xsl:when>
<xsl:otherwise><!-- not development -->
<xsl:choose>
<xsl:when test="$mode = 'valentine'">
<xsl:element name="link">
<xsl:attribute name="rel">stylesheet</xsl:attribute>
<xsl:attribute name="media">all</xsl:attribute>
<xsl:attribute name="href"><xsl:value-of select="$urlprefix"/>/look/valentine.min.css</xsl:attribute>
<xsl:attribute name="type">text/css</xsl:attribute>
</xsl:element>
</xsl:when>
<xsl:otherwise><!-- not valentine -->
<xsl:element name="link">
<xsl:attribute name="rel">stylesheet</xsl:attribute>
<xsl:attribute name="media">all</xsl:attribute>
<xsl:attribute name="href"><xsl:value-of select="$urlprefix"/>/look/fsfe.min.css?20230215</xsl:attribute>
<xsl:attribute name="type">text/css</xsl:attribute>
</xsl:element>
</xsl:otherwise>
</xsl:choose>
<xsl:otherwise><!-- not valentine -->
<xsl:element name="link">
<xsl:attribute name="rel">stylesheet</xsl:attribute>
<xsl:attribute name="media">all</xsl:attribute>
<xsl:attribute name="href"><xsl:value-of select="$urlprefix"/>/look/fsfe.min.css?20230215</xsl:attribute>
<xsl:attribute name="type">text/css</xsl:attribute>
</xsl:element>
</xsl:otherwise>
</xsl:choose>

View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- ====================================================================== -->
<!-- XSL script to extract the <localmenu> dir attribute of an XML file -->
<!-- ====================================================================== -->
<!-- This XSL script outputs the "dir" attribute of the <localmenu> element -->
<!-- of an XML file. It is used by the script tools/update_localmenus.sh. -->
<!-- ====================================================================== -->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:template match="localmenu">
<xsl:value-of select="@dir"/>
</xsl:template>
<!-- Suppress output of text nodes, which would be the default -->
<xsl:template match="text()"/>
</xsl:stylesheet>

View File

@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- ====================================================================== -->
<!-- XSL script to extract the <localmenu> element of an XML file -->
<!-- ====================================================================== -->
<!-- This XSL script outputs a line for the .localmenu.en.xml file from the -->
<!-- <localmenu> element of an .xhtml file. It is used by the script -->
<!-- tools/update_localmenus.sh. -->
<!-- ====================================================================== -->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:template match="localmenu[@id]">
<xsl:text> &lt;localmenuitem set="</xsl:text>
<xsl:choose>
<xsl:when test="@set">
<xsl:value-of select="@set"/>
</xsl:when>
<xsl:otherwise>
<xsl:text>default</xsl:text>
</xsl:otherwise>
</xsl:choose>
<xsl:text>" id="</xsl:text>
<xsl:value-of select="@id"/>
<xsl:text>" link="</xsl:text>
<xsl:value-of select="$link"/>
<xsl:text>"&gt;</xsl:text>
<xsl:value-of select="normalize-space(node())"/>
<xsl:text>&lt;/localmenuitem&gt;</xsl:text>
</xsl:template>
<!-- Suppress output of text nodes, which would be the default -->
<xsl:template match="text()"/>
</xsl:stylesheet>

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- ====================================================================== -->
<!-- XSL script to extract the used modules from a .xhtml file -->
<!-- ====================================================================== -->
<!-- This XSL script processes all <module> elements of a .xhtml file and -->
<!-- outputs the source files for these modules, separated by newlines. -->
<!-- It is used by the script tools/update_xmllists.sh. -->
<!-- ====================================================================== -->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:template match="module">
<!-- Directory name -->
<xsl:text>global/data/modules/</xsl:text>
<!-- Filename = module id -->
<xsl:value-of select="@id"/>
<!-- Append a newline -->
<xsl:text>&#xa;</xsl:text>
</xsl:template>
<!-- Suppress output of text nodes, which would be the default -->
<xsl:template match="text()"/>
</xsl:stylesheet>

View File

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- ====================================================================== -->
<!-- XSL script to extract the content of <tag> elements from an XML file -->
<!-- ====================================================================== -->
<!-- This XSL script processes all <tag> elements of an XML file and -->
<!-- outputs the content of each of these elements, separated by newlines. -->
<!-- It is used by the script tools/update_xmllists.sh. -->
<!-- ====================================================================== -->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:template match="tag">
<!-- Output tag name with some safeguarding against invalid characters -->
<xsl:value-of select="translate(@key, ' /:', '---')"/>
<!-- Output a blank -->
<xsl:text> </xsl:text>
<!-- Output tag label -->
<xsl:value-of select="."/>
<!-- Append a newline -->
<xsl:text>&#xa;</xsl:text>
</xsl:template>
<!-- Suppress output of text nodes, which would be the default -->
<xsl:template match="text()"/>
</xsl:stylesheet>

View File

@ -3,7 +3,6 @@ services:
build: .
image: fsfe-websites
container_name: fsfe-websites
command:
ports:
- 2000:2000
- 2100:2100
@ -11,5 +10,18 @@ services:
- 2300:2300
- 2400:2400
- 2500:2500
secrets:
- KEY_PRIVATE
- KEY_PASSWORD
- GIT_TOKEN
volumes:
- ./:/fsfe-websites
- website-cached:/website-cached
volumes:
website-cached:
secrets:
KEY_PRIVATE:
environment: KEY_PRIVATE
KEY_PASSWORD:
environment: KEY_PASSWORD
GIT_TOKEN:
environment: GIT_TOKEN

46
entrypoint.sh Normal file
View File

@ -0,0 +1,46 @@
#!/usr/bin/env bash
set -euo pipefail
# Ran by dockerfile as entrypoint
# Ran from the volume of the website source mounted at /website-source
# Load sshkeys
if [ -f /run/secrets/KEY_PRIVATE ]; then
# Start ssh-agent
eval "$(ssh-agent)"
# Create config file with required keys
mkdir -p ~/.ssh
echo "AddKeysToAgent yes" > ~/.ssh/config
# Tighten permissions to keep ssh-add happy
chmod 400 /run/secrets/KEY_*
PASSWORD="$(cat "/run/secrets/KEY_PASSWORD")"
PRIVATE="$(cat "/run/secrets/KEY_PRIVATE")"
# Really should be able to just read from the private path, but for some reason ssh-add fails when using the actual path
# But works when you cat the path into another file and then load it
# Or cat the file and pipe it in through stdin
# Piping stdin to an expect command is quite complex, so we just make and remove a temporary key file.
# Absolutely bizarre, and not quite ideal security wise
echo "$PRIVATE" >/tmp/key
chmod 600 /tmp/key
# Use our wrapper expect script to handle interactive input
./exp.exp "$PASSWORD" ssh-add "/tmp/key"
rm /tmp/key
echo "SSH Key Loaded"
else
echo "Secret not defined!"
fi
if [ -f /run/secrets/GIT_TOKEN ]; then
export GIT_TOKEN="$(cat "/run/secrets/GIT_TOKEN")"
fi
# Rsync files over, do not use the mtimes as they are wrong due to docker shenanigans
# Use the .gitignore as a filter to not remove any files generated by previous runs
rsync -rlpgoDz --delete --checksum --filter=':- .gitignore' ./ /website-cached/source
# Change to source repo
cd /website-cached/source
# run build script expaning all args passed to this script
python3 ./build.py "$@"

12
exp.exp Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/env expect
set timeout 20
set cmd [lrange $argv 1 end]
set password [lindex $argv 0]
eval spawn $cmd
expect "Enter passphrase*:"
send "$password\r"
lassign [wait] pid spawn_id os_error actual_exit_code
interact

View File

@ -1,39 +0,0 @@
# -----------------------------------------------------------------------------
# Makefile for FSFE website build, preparation for events subdirectory
# -----------------------------------------------------------------------------
.PHONY: all
.SECONDEXPANSION:
# -----------------------------------------------------------------------------
# Copy event archive template to each of the years
# -----------------------------------------------------------------------------
# All years for which a subdirectory exists
ARCH_YEARS := $(sort $(wildcard [0-9][0-9][0-9][0-9]))
# No archive for the current year
ARCH_YEARS := $(filter-out $(lastword $(ARCH_YEARS)),$(ARCH_YEARS))
# ... and the year before
ARCH_YEARS := $(filter-out $(lastword $(ARCH_YEARS)),$(ARCH_YEARS))
# Languages in which the template exists
ARCH_LANGS := $(filter $(foreach lang,$(languages),.$(lang)),$(suffix $(basename $(wildcard archive-template.??.xhtml))))
# .xhtml files to generate
ARCH_XHTML := $(foreach year,$(ARCH_YEARS),$(foreach lang,$(ARCH_LANGS),$(year)/index$(lang).xhtml))
all: $(ARCH_XHTML)
$(ARCH_XHTML): %.xhtml: archive-template$$(suffix $$*).xhtml
@echo "* Creating $@"
@# $(dir $@) returns YYYY/, we abuse the slash for closing the sed command
@sed 's/:YYYY:/$(dir $@)g' $< > $@
# .sources files to generate
ARCH_SOURCES := $(foreach year,$(ARCH_YEARS),$(year)/index.sources)
all: $(ARCH_SOURCES)
$(ARCH_SOURCES): %.sources:
@echo "* Creating $@"
@echo "fsfe.org/events/$(dir $@)event-*:[]\nfsfe.org/events/$(dir $@).event-*:[]\nfsfe.org/events/.localmenu:[]\n" > $@

View File

@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
# __init__.py is a special Python file that allows a directory to become
# a Python package so it can be accessed using the 'import' statement.

51
fsfe.org/events/subdir.py Normal file
View File

@ -0,0 +1,51 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import multiprocessing
from pathlib import Path
from textwrap import dedent
logger = logging.getLogger(__name__)
def _gen_archive_index(working_dir: Path, languages: list[str], dir: Path):
logger.debug(f"Operating on dir {dir}")
for lang in languages:
logger.debug(f"Operating on lang {lang}")
template = working_dir.joinpath(f"archive-template.{lang}.xhtml")
if template.exists():
logger.debug("Template Exists!")
content = template.read_text()
content = content.replace(":YYYY:", dir.name)
dir.joinpath(f"index.{lang}.xhtml").write_text(content)
def _gen_index_sources(dir: Path):
dir.joinpath("index.sources").write_text(
dedent(
f"""\
{dir}/event-*:[]
{dir}/.event-*:[]
{dir.parent}/.localmenu:[]
"""
)
)
def run(languages: list[str], processes: int, working_dir: Path) -> None:
"""
preparation for news subdirectory
"""
with multiprocessing.Pool(processes) as pool:
years = list(sorted(working_dir.glob("[0-9][0-9][0-9][0-9]")))
# Copy news archive template to each of the years
pool.starmap(
_gen_archive_index,
[(working_dir, languages, dir) for dir in years[:-2]],
)
logger.debug("Finished Archiving")
# Generate index.sources for every year
pool.map(_gen_index_sources, years)
logger.debug("Finished generating sources")

View File

@ -146,6 +146,7 @@
<td><input type="text" name="amount[]" class="form-control" pattern="-?\d{0,5},\d{2}" placeholder="12,34" required="required" /></td>
<td><input type="text" name="recipient[]" class="form-control" required="required" /></td>
<td><select class="form-control col-sm-3" name="activity[]" id="activity" size="1" required="required">
<option />
<module id="fsfe-activities-options" />
</select></td>
<td><select class="form-control col-sm-3" name="category[]" id="category" size="1">

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<html newsdate="2025-03-24">
<html newsdate="2025-04-24">
<version>1</version>
<head>
@ -131,10 +131,10 @@ of the speaker or any of the participants involved in the discussion.
<tag key="CRA">Cyber Resilience Act</tag>
</tags>
<discussion href="https://mastodon.social/@fsfe/"/>
<discussion href="https://mastodon.social/@fsfe/114392294984711318"/>
<image url="https://pics.fsfe.org/uploads/original/bf/5f/659cae1be6e8d3c90cd6d4c50919.jpeg" alt="A group of diverse people, male and female, sitting on the edge of
a stage with a slide with the LLW25 donors on the background and a LLW
<image url="https://pics.fsfe.org/uploads/original/bf/5f/659cae1be6e8d3c90cd6d4c50919.jpeg" alt="A group of diverse people, male and female, sitting on the edge of
a stage with a slide with the LLW25 donors on the background and a LLW
roll up on the right side and a FSFE roll up on the left side"/>
</html>

View File

@ -1,74 +0,0 @@
# -----------------------------------------------------------------------------
# Makefile for FSFE website build, preparation for news subdirectory
# -----------------------------------------------------------------------------
.PHONY: all
.SECONDEXPANSION:
# -----------------------------------------------------------------------------
# Copy news archive template to each of the years
# -----------------------------------------------------------------------------
# All years for which a subdirectory exists
ARCH_YEARS := $(sort $(wildcard [0-9][0-9][0-9][0-9]))
# No archive for the current year
ARCH_YEARS := $(filter-out $(lastword $(ARCH_YEARS)),$(ARCH_YEARS))
# ... and the year before
ARCH_YEARS := $(filter-out $(lastword $(ARCH_YEARS)),$(ARCH_YEARS))
# Languages in which the template exists
ARCH_LANGS := $(filter $(foreach lang,$(languages),.$(lang)),$(suffix $(basename $(wildcard archive-template.??.xhtml))))
# .xhtml files to generate
ARCH_XHTML := $(foreach year,$(ARCH_YEARS),$(foreach lang,$(ARCH_LANGS),$(year)/index$(lang).xhtml))
all: $(ARCH_XHTML)
$(ARCH_XHTML): %.xhtml: archive-template$$(suffix $$*).xhtml
@echo "* Creating $@"
@# $(dir $@) returns YYYY/, we abuse the slash for closing the sed command
@sed 's/:YYYY:/$(dir $@)g' $< > $@
# .sources files to generate
ARCH_SOURCES := $(foreach year,$(ARCH_YEARS),$(year)/index.sources)
all: $(ARCH_SOURCES)
$(ARCH_SOURCES): %.sources:
@echo "* Creating $@"
@printf "fsfe.org/news/$(dir $@)news-*:[]\nfsfe.org/news/$(dir $@).news-*:[]\nfsfe.org/news/.localmenu:[]\n" > $@
# -----------------------------------------------------------------------------
# Remove generated .xml files where original .xhtml file does not exist anymore
# -----------------------------------------------------------------------------
# note the reversal of target <-> prerequisite relationship
# make will execute thew command for all xhtml files (targets) that
# do not exist, in doing so it will not make the target, but rather
# remove the xml file that generated it
# All currently existing generated .xml files
GENERATED_XML := $(wildcard */.*.xml)
# List of corresponding source files (foo/.bar.xx.xml -> foo/bar.xx.xhtml)
GENERATED_XML_SOURCES := $(patsubst %.xml,%.xhtml,$(subst /.,/,$(GENERATED_XML)))
all: $(GENERATED_XML_SOURCES)
%.xhtml:
@echo '* Removing $(subst /,/.,$*).xml'
@rm '$(subst /,/.,$*).xml'
# -----------------------------------------------------------------------------
# Generate .xml files from .xhtml files
# -----------------------------------------------------------------------------
# All existing .xhtml files
XHTML := $(filter $(foreach lang,$(languages),%.$(lang).xhtml),$(shell ls */*.??.xhtml | xargs grep -l '<html newsdate'))
# List of .xml files to generate
XML := $(patsubst %.xhtml,%.xml,$(subst /,/.,$(XHTML)))
all: $(XML)
XMLSOURCE = $(patsubst %.xml,%.xhtml,$(subst /.,/,$@))
%.xml: $$(XMLSOURCE) xhtml2xml.xsl
@echo '* Generating $@'
@xsltproc --stringparam link '/news/$(basename $(basename $<)).html' xhtml2xml.xsl '$<' > '$@'

View File

@ -1,12 +1,12 @@
Adding news
# Adding news
===========
There are two ways to add news to the web pages:
**1**
## 1
Add an xml file with the following structure in the appropriate
directory.
```xml
<?xml version="1.0" encoding="UTF-8"?> (you can choose an other encoding)
<newsset>
@ -18,25 +18,27 @@ directory.
<link>link</link>
</news>
</newsset>
```
Put this file in the directory /news/this_year/
There's a naming convention for these xml files:
```
'news-'newsdate'-'counter'.'language_part'.xml'
```
(eg: the English version of the first news file on the 4th November of
2008 should be named news-20081104-01.en.xml and the file should go in
the /news/2008/ directory)
**2**
## 2
Add an xhtml file in the appropriate directory.
Write an ordinary xhtml file. Add the newsdate="date" attribute in the
xhtml tag. The first <p> element will be copied into the xml file.
(eg:
```xhtml
<?xml versio ...
<html newsdate="2008-10-07" link="link" > (link attribute is optional)
@ -54,7 +56,7 @@ xhtml tag. The first <p> element will be copied into the xml file.
....
</html>
```
The link in the generated xml file will default to the original
page. If you want it to link to another page then you can use the
link attribute in the html tag.

View File

@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
# __init__.py is a special Python file that allows a directory to become
# a Python package so it can be accessed using the 'import' statement.

86
fsfe.org/news/subdir.py Normal file
View File

@ -0,0 +1,86 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
import multiprocessing
from pathlib import Path
from textwrap import dedent
import lxml.etree as etree
from build.lib.misc import lang_from_filename, update_if_changed
logger = logging.getLogger(__name__)
def _gen_archive_index(working_dir: Path, languages: list[str], dir: Path):
logger.debug(f"Operating on dir {dir}")
for lang in languages:
logger.debug(f"Operating on lang {lang}")
template = working_dir.joinpath(f"archive-template.{lang}.xhtml")
if template.exists():
logger.debug("Template Exists!")
content = template.read_text()
content = content.replace(":YYYY:", dir.name)
dir.joinpath(f"index.{lang}.xhtml").write_text(content)
def _gen_index_sources(dir: Path):
dir.joinpath("index.sources").write_text(
dedent(
f"""\
{dir}/news-*:[]
{dir}/.news-*:[]
{dir.parent}/.localmenu:[]
"""
)
)
def _gen_xml_files(working_dir: Path, file: Path):
logger.debug(f"Transforming {file}")
# Would be more efficient to pass this to the function, but this causes a pickling error, and the faq seems to indicate passing around these objects between threads causes issues
# https://lxml.de/5.0/FAQ.html
# So I guess we just have to take the performance hit.
xslt_tree = etree.parse(working_dir.joinpath("xhtml2xml.xsl"))
transform = etree.XSLT(xslt_tree)
result = transform(
etree.parse(file),
link=f"'/news/{file.with_suffix('').with_suffix('.html').relative_to(file.parent.parent)}'",
)
update_if_changed(
file.parent.joinpath(
f".{file.with_suffix('').stem}{file.with_suffix('').suffix}.xml"
),
str(result),
)
def run(languages: list[str], processes: int, working_dir: Path) -> None:
"""
preparation for news subdirectory
"""
with multiprocessing.Pool(processes) as pool:
years = list(sorted(working_dir.glob("[0-9][0-9][0-9][0-9]")))
# Copy news archive template to each of the years
pool.starmap(
_gen_archive_index,
[(working_dir, languages, dir) for dir in years[:-2]],
)
logger.debug("Finished Archiving")
# Generate index.sources for every year
pool.map(_gen_index_sources, years)
logger.debug("Finished generating sources")
pool.starmap(
_gen_xml_files,
[
(working_dir, file)
for file in filter(
lambda path: lang_from_filename(path) in languages
and etree.parse(path).xpath("//html[@newsdate]"),
working_dir.glob("*/*.??.xhtml"),
)
],
)
logger.debug("Finished generating xml files")

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
&#1575;&#1604;&#1593;&#1585;&#1576;&#1610;&#1617;&#1577;
العربيّة

10
requirements.txt Normal file
View File

@ -0,0 +1,10 @@
# XML parser
lxml==5.3.2
# For getting english language names of languages from two letter codes.
python-iso639==2025.2.18
# For stopwords for the search index
nltk==3.9.1
# For minification html css and js
tdewolff-minify==2.20.37
# For HTTP requests
requests==2.32.3

View File

@ -1,77 +0,0 @@
#!/usr/bin/env python3
import argparse
import http.server
import multiprocessing
import os
import socketserver
def run_webserver(path, PORT):
os.chdir(path)
Handler = http.server.SimpleHTTPRequestHandler
httpd = socketserver.TCPServer(("0.0.0.0", PORT), Handler)
httpd.serve_forever()
return
def main():
# Change dir to dir of script
abspath = os.path.abspath(__file__)
dname = os.path.dirname(abspath)
os.chdir(dname)
# Arguments:
parser = argparse.ArgumentParser(
description="Serve all directories in a folder over http"
)
parser.add_argument(
"--serve-dir",
dest="serve_dir",
help="Directory to serve webpages from",
type=str,
default="./output/final",
)
parser.add_argument(
"--base-port",
dest="base_port",
help="Initial value of http port",
type=int,
default=2000,
)
parser.add_argument(
"--increment-number",
dest="increment_number",
help="Number to increment port num by per webpage",
type=int,
default=100,
)
args = parser.parse_args()
dirs = list(
filter(
lambda item: os.path.isdir(item),
map(lambda item: args.serve_dir + "/" + item, os.listdir(args.serve_dir)),
)
)
servers = []
for dir in dirs:
port = args.base_port + (args.increment_number * dirs.index(dir))
print(f"{dir} will be served on http://127.0.0.1:{port}")
p = multiprocessing.Process(
target=run_webserver,
args=(dir, port),
)
servers.append(p)
for server in servers:
server.start()
for server in servers:
server.join()
if __name__ == "__main__":
main()

View File

@ -4,25 +4,49 @@
overlays = [ ];
},
}:
let
treefmt-nixSrc = builtins.fetchTarball "https://github.com/numtide/treefmt-nix/archive/refs/heads/master.tar.gz";
treefmt-nix = import treefmt-nixSrc;
in
pkgs.mkShell {
nativeBuildInputs = with pkgs; [
# Needed for standard build
coreutils
findutils
gnused
gnugrep
gnumake
rsync
# The main required tool python
python3
# needed by lxml
libxslt
libxml2
iconv
wget
# Needed for the site index script
python312
python312Packages.beautifulsoup4
# Needed only for non dev builds. IE --build-env "fsfe.org" or such
# For less compilation
lessc
# Needed for translation status script
perl
# Needed for git clean in full rebuilds
git
# Needed for compiling minifiers
libffi
go
# Formatter
(treefmt-nix.mkWrapper pkgs {
# Used to find the project root
projectRootFile = "shell.nix";
enableDefaultExcludes = true;
programs = {
ruff-check.enable = true;
ruff-format.enable = true;
nixfmt.enable = true;
};
settings = {
global = {
on-unmatched = "debug";
excludes = [
".nltk_data"
".venv"
];
};
};
})
];
shellHook = ''
export PIP_DISABLE_PIP_VERSION_CHECK=1;
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
'';
}

View File

@ -1,203 +0,0 @@
#!/usr/bin/env bash
exec 2>/dev/null
DATADIR="data"
readonly DATADIR
if [ "$QUERY_STRING" = "full_build" ]; then
if printf %s "$HTTP_REFERER" | grep -qE '^https?://([^/]+\.)?fsfe\.org/'; then
touch ./"$DATADIR"/full_build
fi
printf 'Location: ./\n\n'
exit 0
fi
timestamp() {
date -d "@$1" +"%F %T (%Z)"
}
DATADIR="data"
readonly DATADIR
duration() {
minutes=$(($1 / 60))
if [ "${minutes}" == "1" ]; then
minutes="${minutes} minute"
else
minutes="${minutes} minutes"
fi
seconds=$(($1 % 60))
if [ "${seconds}" == "1" ]; then
seconds="${seconds} second"
else
seconds="${seconds} seconds"
fi
echo "${minutes} ${seconds}"
}
htmlcat() {
sed 's;&;\&amp\;;g;
s;<;\&lt\;;g;
s;>;\&gt\;;g;
s;";\&quot\;;g;
s;'\'';\&apos\;;g;' $@
}
start_time=$(cat "$DATADIR/start_time" || stat -c %Y "$0" || echo 0)
t_gitupdate=$(stat -c %Y "$DATADIR/GITlatest" || echo 0)
t_phase_1=$(stat -c %Y "$DATADIR/phase_1" || echo 0)
t_makefile=$(stat -c %Y "$DATADIR/Makefile" || echo 0)
t_phase_2=$(stat -c %Y "$DATADIR/phase_2" || echo 0)
t_manifest=$(stat -c %Y "$DATADIR/manifest" || echo 0)
t_stagesync=$(stat -c %Y "$DATADIR/stagesync" || echo 0)
end_time=$(cat "$DATADIR/end_time" || echo 0)
duration=$(($end_time - $start_time))
term_status=$(if [ "$duration" -gt 0 -a -f "$DATADIR"/lasterror ]; then
echo Error
elif [ "$duration" -gt 0 ]; then
echo Success
fi)
printf %s\\n\\n "Content-Type: text/html;charset=utf-8"
sed -e '/<!--\ spacing-comment\ -->/,$d' template.en.html
cat <<-HTML_END
<h1>Build report</h1>
<dl class="buildinfo">
<dt>Start time:</dt><dd>$(timestamp ${start_time})</dd>
<dt>End time:</dt><dd>$([ "$duration" -gt 0 ] && timestamp ${end_time})</dd>
<dt>Duration:</dt><dd>$([ "$duration" -gt 0 ] && duration ${duration})</dd>
<dt>Termination Status:</dt><dd>${term_status:-running...}</dd>
</dl>
$(if [ -f ./$DATADIR/full_build ]; then
printf '<span class="fullbuild">Full rebuild will be started within next minute.</span>'
else
printf '<a class="fullbuild" href="./?full_build">Schedule full rebuild</a>'
fi)
<details>
<summary>Previous builds</summary>
<div class="scrollbox">
<a href="/${PWD##*/}">latest</a><br/>
$(
find "$DATADIR" -name "status_*.html" -type f -printf "%f\n" | sort -r | head -n10 | while read stat; do
t="${stat#status_}"
t="${t%.html}"
printf '<a href="%s">%s</a> - %s<br/>' \
"/${PWD##*/}/${DATADIR}/$stat" "$(timestamp "$t")" "$(sed -rn 's;^.*<dt>Duration:</dt><dd>(.+)</dd>.*$;\1;p;T;q' "$stat")"
printf $'\n'
done
)
</div>
</details>
<details>
<summary>GIT changes:
$(
if [ ${start_time} -le ${t_gitupdate} ]; then
echo "at $(timestamp ${t_gitupdate})" "</summary>" \
"<div class=\"scrollbox\">" \
"<pre>$(htmlcat "$DATADIR"/GITlatest)</pre>" \
"checked"
else
echo "Unconditional build, changes ignored" \
"</summary>" \
"<div class=\"scrollbox\">"
fi
)
</div>
</details>
<details>
<summary>Phase 1:
$(
if [ $start_time -lt $t_phase_1 -a $start_time -lt $t_gitupdate ]; then
echo "$(duration $(($t_phase_1 - $t_gitupdate)))" "</summary>" \
"<div class=\"scrollbox\">" \
"<pre>$(htmlcat "$DATADIR"/phase_1)</pre>"
elif [ $start_time -lt $t_phase_1 ]; then
echo "$(duration $(($t_phase_1 - $start_time)))" "</summary>" \
"<div class=\"scrollbox\">" \
"<pre>$(htmlcat "$DATADIR"/phase_1)</pre>"
else
echo "waiting" "</summary>" \
"<div class=\"scrollbox\">"
fi
)
</div>
</details>
<details>
<summary>Phase 2 Makefile:
$(
if [ $start_time -lt $t_makefile ]; then
echo "$(duration $(($t_makefile - $t_phase_1)))" "</summary>" \
"<div class=\"scrollbox\">" \
"<pre>$(htmlcat "$DATADIR"/Makefile)</pre>"
else
echo "waiting" "</summary>" \
"<div class=\"scrollbox\">"
fi
)
</div>
</details>
<details>
<summary>Phase 2:
$(
if [ $start_time -lt $t_phase_2 ]; then
echo "$(duration $(($t_phase_2 - $t_makefile)))" "</summary>" \
"<div class=\"scrollbox\">" \
"<pre>$(htmlcat "$DATADIR"/phase_2)</pre>"
else
echo "waiting" "</summary>" \
"<div class=\"scrollbox\">"
fi
)
</div>
</details>
<details>
<summary>Target update:
$(
if [ ${start_time} -lt ${t_stagesync} -a -s "$DATADIR"/stagesync ]; then
echo "$(($(wc -l "$DATADIR"/stagesync | cut -f1 -d\ ) - 4)) updated files" "</summary>" \
"<div class=\"scrollbox\">" \
"<pre>$(htmlcat "$DATADIR"/stagesync)</pre>"
elif [ -z ${term_status} ]; then
echo "waiting" "</summary>" \
"<div class=\"scrollbox\">"
else
echo "-" "</summary>" \
"<div class=\"scrollbox\">"
fi
)
</div>
</details>
<details>
<summary>Errors:
$(
if [ -f lasterror ]; then
echo "There were errors" "</summary>" \
"<div class=\"scrollbox\">" \
"<pre>$(htmlcat "$DATADIR"/lasterror)</pre>" \
"checked"
else
echo "none" "</summary>" \
"<div class=\"scrollbox\">"
fi
)
</div>
</details>
<details>
<summary>File Manifest:
$(
if [ $start_time -lt $t_manifest ]; then
echo "$(wc -l < "$DATADIR"/manifest) files" "</summary>" \
"<div class=\"scrollbox\">" \
"<a href=\"$DATADIR/manifest\">view</a>"
else
echo "waiting" "</summary>" \
"<div class=\"scrollbox\">"
fi
)
</div>
</details>
HTML_END
sed -n -e '/<\/body>/,$p' template.en.html

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<html external="true">
<version>0</version>
<head>
<title>Build Status</title>
</head>
<body>
<!-- spacing-comment -->
</body>
</html>

View File

@ -32,19 +32,16 @@
<h2>Website status</h2>
<p>The following status information is available:</p>
<h2>fsfe.org</h2>
<h2>Builds</h2>
<ul>
<li>
<a href="fsfe.org/index.cgi">Last build script output</a>
</li>
<li>
<a href="translations/">Translation status</a>
<a href="https://drone.fsfe.org/FSFE/fsfe-website">Builds of the website</a>
</li>
</ul>
<h2>test.fsfe.org</h2>
<h2>Translations</h2>
<ul>
<li>
<a href="test.fsfe.org/index.cgi">Last build script output</a>
<a href="translations/">Translation status</a>
</li>
</ul>
</body>

View File

@ -1 +0,0 @@
../fsfe.org/index.cgi

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<html external="true">
<version>0</version>
<head>
<title>Build Status</title>
</head>
<body>
<!-- spacing-comment -->
</body>
</html>

View File

@ -1,25 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Makes all access/modified file dates in the whole repository match with the file's last git commit date
# This is important because Make is based on file timestamps, not git commits
# We have the -z flag so that git does not replace unicode chars with escape codes, and quote the filenames
# This also uses null bytes instead of newlines, so we swap them
# Only operate on xml and xhtml files. Faster, and all thats needed for translation-status.sh
files=$(git ls-files -z "$(git rev-parse --show-toplevel)" | sed 's/\x0/\n/g' | grep -E '^[a-z\.]+\.[a-z]+/.*(xml|xhtml)$')
total=$(echo "$files" | wc -l)
i=1
echo "$files" | while read -r file; do
echo "[${i}/${total}] $file"
# TODO the line directly below this is because after moving main website to fsfe.org dir the translation status
# stuff based on dates became a bit useless.
# So we use the second to last commit date for every file.
# after 6 months or so (february 2025) remove the line below with --follow in it, and uncomment the touch underneath it
# TLDR: If after February 2025 remove line directly below this, containign follow and uncomment the touch line below that,
# without a follow. Please also remove this comment then
touch -a -m --date="@$(git log --pretty="%ct" --follow -2 "$file"| tail -n1)" "$file"
# touch -a -m --date="@$(git log --pretty="%ct" -1 "$file")" "$file"
((i++))
done

View File

@ -1,225 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
while getopts h OPT; do
case $OPT in
h)
print_usage
exit 0
;;
*)
echo "Unknown option: -$OPTARG"
print_usage
exit 1
;;
esac
done
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
readonly SCRIPT_DIR
REPO="$SCRIPT_DIR"/../..
readonly REPO
OUT="${REPO}"/status.fsfe.org/translations/data
readonly OUT
OUT_TMP="${REPO}"/status.fsfe.org/translations/data-tmp
readonly OUT_TMP
cd "${REPO}" || exit 2
echo "Making required directories!"
mkdir -p "$OUT"
mkdir -p "$OUT_TMP"
LOGFILE="${OUT_TMP}/log.txt"
langs="$(
find ./global/languages -type f -printf "%f\n" | while read -r lang; do
echo "$lang $(cat ./global/languages/"$lang")"
done
)"
readonly langs
texts_dir="global/data/texts"
readonly texts_dir
texts_en=$(grep 'id=".*"' ${texts_dir}/texts.en.xml | perl -pe 's/.*id=\"(.*?)\".*/\1/g')
readonly texts_en
# Make filedates match git commits
echo "Begin syncing filedates with git commit dates" | tee "${LOGFILE}"
"$SCRIPT_DIR"/filedate-sync-git.sh >>"${LOGFILE}"
echo "File date sync finished" | tee -a "${LOGFILE}"
files=""
# Grouped by priority
files+=$'\n'$(find ./fsfe.org/index.en.xhtml | sed 's/$/ 1/')
files+=$'\n'$(find ./fsfe.org/freesoftware/freesoftware.en.xhtml | sed 's/$/ 1/')
files+=$'\n'$(find ./fsfe.org/activities -type f \( -iname "activity\.en\.xml" \) | sed 's/$/ 2/')
files+=$'\n'$(find ./fsfe.org/activities -type f \( -iname "*\.en\.xhtml" -o -iname "*\.en\.xml" \) | sed 's/$/ 3/')
files+=$'\n'$(find ./fsfe.org/freesoftware -type f \( -iname "*\.en\.xhtml" -o -iname "*\.en\.xml" \) | sed 's/$/ 3/')
files+=$'\n'$(find ./fsfe.org/order -maxdepth 1 -type f \( -iname "*\.en\.xhtml" -o -iname "*\.en\.xml" \) | sed 's/$/ 4/')
files+=$'\n'$(find ./fsfe.org/contribute -maxdepth 1 -type f \( -iname "spreadtheword*\.en\.xhtml" -o -iname "spreadtheword*\.en\.xml" \) | sed 's/$/ 4/')
files+=$'\n'$(find ./fsfe.org -type f \( -iname "*\.en\.xhtml" -o -iname "*\.en\.xml" \) -mtime -200 -path './fsfe.org/news' -prune -path './fsfe.org/events' -prune | sed 's/$/ 5/')
files+=$'\n'$(find ./fsfe.org/news -type f \( -iname "*\.en\.xhtml" -o -iname "*\.en\.xml" \) -mtime -30 | sed 's/$/ 5/')
# Remove files that are not in the list of those managed by git
tmp=""
files="$(
echo "$files" | while read -r line priority; do
if [[ "$(git ls-files | grep -o "${line:2}" | wc -l)" -ge 1 ]] && [[ "$(echo "$tmp" | grep "$line")" == "" ]]; then
echo "$line" "$priority"
tmp+="$line"
fi
done
)"
unset tmp
files=$(echo "$files" | grep -v "internal\|order/data/items\.en\.xml\|donate/germany\|donate/netherlands\|donate/switzerland\|status.fsfe.org\|boilerplate\|/\..*\.xml\|)")
readonly files
prevlang=""
prevprio=""
echo "Begin generating translation status for all languages" | tee -a "$LOGFILE"
statuses="$(
echo "$files" | while read -r fullname priority; do
ext="${fullname##*.}"
base="${fullname//\.[a-z][a-z]\.${ext}/}"
original_version=$(xsltproc build/xslt/get_version.xsl "$base".en."$ext")
echo "$langs" | while read -r lang_short lang_long; do
i="$base"."$lang_short"."$ext"
echo "Processing file $i" >>"$LOGFILE"
if [[ -f $i ]]; then
translation_version=$(xsltproc build/xslt/get_version.xsl "$i")
else
translation_version="-1"
fi
if [ "${translation_version:-0}" -lt "${original_version:-0}" ]; then
# TODO the line directly below this is because after moving main website to fsfe.org dir the translation status
# stuff based on dates became a bit useless.
# So we use the second to last commit date for every file.
# after 6 months or so (february 2025) remove the line below with --follow in it, and uncomment the touch underneath it
# TLDR: If after February 2025 remove line directly below this, containign follow and uncomment the touch line below that,
# without a follow. Please also remove this comment then
originaldate=$(git log --follow --pretty="%ct" -2 "$base".en."$ext" | tail -n1)
# originaldate=$(git log --pretty="%ct" -1 "$base".en."$ext")
if [ "$ext" == "xhtml" ]; then
original_url="https://webpreview.fsfe.org?uri=${base/#\.\/fsfe.org/}.en.html"
translation_url="https://webpreview.fsfe.org?uri=${base/#\.\/fsfe.org/}.$lang_short.html"
elif [ "$ext" == "xml" ]; then
original_url="https://git.fsfe.org/FSFE/fsfe-website/src/branch/master/${base/#\.\//}.en.xml"
translation_url="https://git.fsfe.org/FSFE/fsfe-website/src/branch/master/${base/#\.\//}.$lang_short.xml"
else
translation_url="#"
original_url="#"
fi
if [ "$translation_version" == "-1" ]; then
translation_url="#"
fi
echo "$lang_short $lang_long $base $priority $originaldate $original_url $original_version $translation_url ${translation_version/-1/Untranslated}"
fi
done
done | sort -t' ' -k 1,1 -k 4,4 -k 3,3 | cat - <(echo "zz zz zz zz zz zz zz zz zz")
)"
echo "Status Generated" | tee -a "$LOGFILE"
echo "Generate language status overview" | tee -a "$LOGFILE"
cat >"${OUT_TMP}/langs.en.xml" <<-EOF
<?xml version="1.0" encoding="UTF-8"?>
<translation-overall-status>
<version>1</version>
EOF
echo "$langs" | while read -r lang_short lang_long; do
declare -A prio_counts
for i in {1..6}; do
prio_counts["$i"]="<priority number=\"$i\" value=\"$(echo "$statuses" | sed -n "/^$lang_short\ $lang_long\ [^\ ]*\ $i/p" | wc -l)\"/>"
done
cat >>"${OUT_TMP}/langs.en.xml" <<-EOF
<language short="$lang_short" long="$lang_long">
${prio_counts["1"]}
${prio_counts["2"]}
</language>
EOF
unset prio_counts
done
cat >>"${OUT_TMP}/langs.en.xml" <<-EOF
</translation-overall-status>
EOF
echo "Finished Generating status overview" | tee -a "$LOGFILE"
echo "Create language pages" | tee -a "$LOGFILE"
echo "$statuses" | while read -r lang_short lang_long page priority originaldate original_url original_version translation_url translation_version; do
if [[ "$prevlang" != "$lang_short" ]]; then
if [[ "$prevlang" != "" ]]; then
# Translatable strings
texts_file="${texts_dir}/texts.${prevlang}.xml"
missing_texts=()
longest_text_length=0
for text in $texts_en; do
if ! xmllint --xpath "//text[@id = \"${text}\"]" "${texts_file}" &>/dev/null; then
missing_texts+=("$text")
tmp_length="${#text}"
if [ "$tmp_length" -gt "$longest_text_length" ]; then
longest_text_length="$tmp_length"
fi
fi
done
for index in "${!missing_texts[@]}"; do
missing_texts["$index"]="<text>${missing_texts["$index"]}</text>"$'\n'
done
cat >>"${OUT_TMP}/translations.$prevlang.xml" <<-EOF
</priority>
<missing-texts>
<url>https://git.fsfe.org/FSFE/fsfe-website/src/branch/master/${texts_file}</url>
<max-length>$((longest_text_length + 5))ch</max-length>
<filename> $texts_file</filename>
${missing_texts[*]}
</missing-texts>
</translation-status>
EOF
if [[ "$lang_short" == "zz" ]]; then
break
fi
fi
cat >"${OUT_TMP}/translations.$lang_short.xml" <<-EOF
<?xml version="1.0" encoding="UTF-8"?>
<translation-status>
<version>1</version>
EOF
prevprio=""
prevlang=$lang_short
fi
if [[ "$priority" != "$prevprio" ]]; then
if [[ "$prevprio" != "" ]]; then
cat >>"${OUT_TMP}/translations.$lang_short.xml" <<-EOF
</priority>
EOF
fi
cat >>"${OUT_TMP}/translations.$lang_short.xml" <<-EOF
<priority value="$priority">
EOF
prevprio=$priority
fi
orig=$(date +"%Y-%m-%d" --date="@$originaldate")
if [[ $originaldate -gt $(date +%s --date='6 months ago') ]]; then
new=true
else
new=false
fi
cat >>"${OUT_TMP}/translations.$lang_short.xml" <<-EOF
<file>
<page>$page</page>
<original_date>$orig</original_date>
<new>$new</new>
<original_url>$original_url</original_url>
<original_version>$original_version</original_version>
<translation_url>$translation_url</translation_url>
<translation_version>$translation_version</translation_version>
</file>
EOF
done
echo "Finished creating language pages" | tee -a "$LOGFILE"
rsync -avz --delete --remove-source-files "$OUT_TMP"/ "$OUT"/
echo "Finished !"

View File

@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
# __init__.py is a special Python file that allows a directory to become
# a Python package so it can be accessed using the 'import' statement.

View File

@ -36,6 +36,7 @@
</table>
</details>
</xsl:template>
<xsl:template match="translation-status">
<xsl:if test="/buildinfo/@language != 'en'">
<div class="translation-status">
@ -61,31 +62,31 @@
<xsl:for-each select="file">
<tr>
<td>
<xsl:value-of select="page"/>
<xsl:value-of select="@page"/>
</td>
<td>
<xsl:value-of select="original_date"/>
<xsl:value-of select="@original_date"/>
</td>
<td>
<a>
<xsl:attribute name="href">
<xsl:value-of select="original_url"/>
<xsl:value-of select="@original_url"/>
</xsl:attribute>
<xsl:value-of select="original_version"/>
<xsl:value-of select="@original_version"/>
</a>
</td>
<td>
<xsl:choose>
<xsl:when test="translation_url != '#'">
<xsl:when test="@translation_url != '#'">
<a>
<xsl:attribute name="href">
<xsl:value-of select="translation_url"/>
<xsl:value-of select="@translation_url"/>
</xsl:attribute>
<xsl:value-of select="translation_version"/>
<xsl:value-of select="@translation_version"/>
</a>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="translation_version"/>
<xsl:value-of select="@translation_version"/>
</xsl:otherwise>
</xsl:choose>
</td>
@ -95,26 +96,27 @@
</details>
</xsl:for-each>
<xsl:for-each select="/buildinfo/document/set/missing-texts">
<xsl:variable name="max-length" select="max-length"/>
<h3>
<xsl:text>Missing texts in </xsl:text>
<a>
<xsl:attribute name="href">
<xsl:value-of select="url"/>
<xsl:value-of select="@url"/>
</xsl:attribute>
<xsl:value-of select="filename"/>
<xsl:value-of select="@filepath"/>
</a>
</h3>
<h4>
<xsl:text>English language texts version: </xsl:text>
<xsl:value-of select="@en"/>
<br/>
<xsl:text>Current language language texts version: </xsl:text>
<xsl:value-of select="@curr_lang"/>
</h4>
<details>
<summary>Show</summary>
<div style="display: flex; flex-wrap: wrap;">
<div style="display: flex; flex-wrap: wrap; align-items: stretch; align-content: stretch; justify-content: space-evenly; gap: 10px;">
<xsl:for-each select="text">
<div>
<xsl:attribute name="style">
<xsl:text>width: </xsl:text>
<xsl:value-of select="$max-length"/>
<xsl:text>;</xsl:text>
</xsl:attribute>
<xsl:value-of select="current()"/>
</div>
</xsl:for-each>
@ -124,4 +126,5 @@
</div>
</xsl:if>
</xsl:template>
</xsl:stylesheet>

View File

@ -0,0 +1,256 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import datetime
import logging
import multiprocessing
import os
from pathlib import Path
import lxml.etree as etree
from build.lib.misc import get_basepath, get_version, run_command, update_if_changed
logger = logging.getLogger(__name__)
def _update_mtime(
file: Path,
) -> None:
logger.debug(f"Updating mtime of {file}")
result = run_command(["git", "log", '--pretty="%ct"', "-1", file])
time = int(result.strip('"'))
os.utime(file, (time, time))
def _generate_translation_data(lang: str, priority: int, file: Path) -> dict:
page = get_basepath(file)
ext = file.suffix.removeprefix(".")
working_file = file.with_suffix("").with_suffix(f".{lang}.{ext}")
original_date = datetime.datetime.fromtimestamp(file.stat().st_mtime)
original_version = str(get_version(file))
translation_version = (
str(get_version(working_file)) if working_file.exists() else "untranslated"
)
original_url = (
f"https://webpreview.fsfe.org?uri=/{page.relative_to(page.parts[0])}.en.html"
if ext == "xhtml"
else (
f"https://git.fsfe.org/FSFE/fsfe-website/src/branch/master/{page}.en.xml"
if ext == "xml"
else "#"
)
)
translation_url = (
"#"
if not working_file.exists()
else (
f"https://webpreview.fsfe.org?uri=/{page.relative_to(page.parts[0])}.{lang}.html"
if ext == "xhtml"
else (
f"https://git.fsfe.org/FSFE/fsfe-website/src/branch/master/{page}.{lang}.xml"
if ext == "xml"
else "#"
)
)
)
return (
{
"page": str(page),
"original_date": str(original_date),
"original_url": str(original_url),
"original_version": str(original_version),
"translation_url": str(translation_url),
"translation_version": str(translation_version),
}
if translation_version != original_version
else None
)
def _get_text_ids(file: Path) -> list[str]:
texts_tree = etree.parse(file)
root = texts_tree.getroot()
return list(
filter(
lambda text_id: text_id is not None,
map(lambda elem: elem.get("id"), root.iter()),
)
)
def _create_overview(
target_dir: Path,
data: dict[str : dict[int : list[dict]]],
):
work_file = target_dir.joinpath("langs.en.xml")
work_file.parent.mkdir(parents=True, exist_ok=True)
# Create the root element
page = etree.Element("translation-overall-status")
# Add the subelements
version = etree.SubElement(page, "version")
version.text = "1"
for lang in data:
language_elem = etree.SubElement(
page,
"language",
short=lang,
long=Path(f"global/languages/{lang}").read_text().strip(),
)
for prio in list(data[lang].keys())[:2]:
etree.SubElement(
language_elem,
"priority",
number=str(prio),
value=str(len(data[lang][prio])),
)
result_str = etree.tostring(page, xml_declaration=True, encoding="utf-8").decode(
"utf-8"
)
update_if_changed(work_file, result_str)
def _create_translation_file(
target_dir: Path,
lang: str,
data: dict[int : list[dict]],
) -> None:
work_file = target_dir.joinpath(f"translations.{lang}.xml")
page = etree.Element("translation-status")
version = etree.SubElement(page, "version")
version.text = "1"
for priority in data:
prio = etree.SubElement(page, "priority", value=str(priority))
for file_data in data[priority]:
etree.SubElement(prio, "file", **file_data)
en_texts_file = Path("global/data/texts/texts.en.xml")
lang_texts_file = Path(f"global/data/texts/texts.{lang}.xml")
en_texts = _get_text_ids(en_texts_file)
lang_texts = _get_text_ids(lang_texts_file) if lang_texts_file.exists() else []
missing_texts_elem = etree.SubElement(
page,
"missing-texts",
en=str(get_version(en_texts_file)),
curr_lang=(
str(get_version(lang_texts_file))
if lang_texts_file.exists()
else "No texts File!"
),
url=f"https://git.fsfe.org/FSFE/fsfe-website/src/branch/master/{lang_texts_file}",
filepath=str(lang_texts_file),
)
for missing_text in filter(lambda id: id not in lang_texts, en_texts):
text_elem = etree.SubElement(missing_texts_elem, "text")
text_elem.text = missing_text
# Save to XML file
result_str = etree.tostring(page, xml_declaration=True, encoding="utf-8").decode(
"utf-8"
)
update_if_changed(work_file, result_str)
def run(languages: list[str], processes: int, working_dir: Path) -> None:
"""
Build translation-status xmls for languages where the translation status has changed. Xmls are placed in target_dir, and only languages are processed.
"""
target_dir = working_dir.joinpath("data/")
logger.debug(f"Building index of status of translations into dir {target_dir}")
result = run_command(
["git", "rev-parse", "--show-toplevel"],
)
# TODO
# Run generating all this stuff only if some xhtml|xml files have been changed
# List files separated by a null bytes
result = run_command(
["git", "ls-files", "-z", result],
)
with multiprocessing.Pool(processes) as pool:
pool.map(
_update_mtime,
filter(
lambda path: path.suffix in [".xhtml", ".xml"],
# Split on null bytes, strip and then parse into path
map(lambda line: Path(line.strip()), result.split("\x00")),
),
)
# Generate our file lists by priority
# Super hardcoded unfortunately
files_by_prio = dict()
files_by_prio[1] = list(Path("fsfe.org/").glob("index.en.xhtml")) + list(
Path("fsfe.org/freesoftware/").glob("freesoftware.en.xhtml")
)
files_by_prio[2] = list(Path("fsfe.org/activities/").glob("*/activity.en.xml"))
files_by_prio[3] = (
list(Path("fsfe.org/activities/").glob("*.en.xhtml"))
+ list(Path("fsfe.org/activities/").glob("*.en.xml"))
+ list(Path("fsfe.org/freesoftware/").glob("*.en.xhtml"))
+ list(Path("fsfe.org/freesoftware/").glob("*.en.xml"))
)
files_by_prio[4] = (
list(Path("fsfe.org/order/").glob("*.en.xml"))
+ list(Path("fsfe.org/order/").glob("*.en.xhtml"))
+ list(Path("fsfe.org/contribute/").glob("*.en.xml"))
+ list(Path("fsfe.org/contribute/").glob("*.en.xhtml"))
)
files_by_prio[5] = list(Path("fsfe.org/order/").glob("**/*.en.xml")) + list(
Path("fsfe.org/order/").glob("**/*.en.xhtml")
)
for priority in sorted(files_by_prio.keys(), reverse=True):
files_by_prio[priority] = list(
filter(
lambda path: not any(
[(path in files_by_prio[prio]) for prio in range(1, priority)]
),
files_by_prio[priority],
)
)
files_by_lang_by_prio = {}
for lang in languages:
files_by_lang_by_prio[lang] = {}
for priority in files_by_prio:
files_by_lang_by_prio[lang][priority] = list(
filter(
lambda result: result is not None,
pool.starmap(
_generate_translation_data,
[
(lang, priority, file)
for file in files_by_prio[priority]
],
),
)
)
# sadly single treaded, as only one file being operated on
_create_overview(target_dir, files_by_lang_by_prio)
for data in [
(target_dir, lang, files_by_lang_by_prio[lang])
for lang in files_by_lang_by_prio
]:
pool.starmap(
_create_translation_file,
[
(target_dir, lang, files_by_lang_by_prio[lang])
for lang in files_by_lang_by_prio
],
)

6
tools/__init__.py Normal file
View File

@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: Free Software Foundation Europe e.V. <https://fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
# __init__.py is a special Python file that allows a directory to become
# a Python package so it can be accessed using the 'import' statement.

File diff suppressed because one or more lines are too long

View File

@ -1,38 +0,0 @@
#!/usr/bin/env bash
# -----------------------------------------------------------------------------
# Update default sytlesheets
# -----------------------------------------------------------------------------
# This script is called from the phase 1 Makefile and places a .default.xsl
# into each directory containing source files for HTML pages (*.xhtml). These
# .default.xsl are symlinks to the first available actual default.xsl found
# when climbing the directory tree upwards, it's the XSL stylesheet to be used
# for building the HTML files from this directory.
# -----------------------------------------------------------------------------
set -e
echo "* Updating default stylesheets"
# -----------------------------------------------------------------------------
# Get a list of all directories containing .xhtml source files
# -----------------------------------------------------------------------------
directories=$(
find -name "*.??.xhtml" \
| xargs dirname \
| sort --unique
)
# -----------------------------------------------------------------------------
# In each dir, place a .default.xsl symlink pointing to the nearest default.xsl
# -----------------------------------------------------------------------------
for directory in ${directories}; do
dir="${directory}"
prefix=""
until [ -f "${dir}/default.xsl" -o "${dir}" = "." ]; do
dir="$(dirname "${dir}")"
prefix="${prefix}../"
done
ln -sf "${prefix}default.xsl" "${directory}/.default.xsl"
done

View File

@ -1,80 +0,0 @@
#!/usr/bin/env bash
# -----------------------------------------------------------------------------
# Update local menus
# -----------------------------------------------------------------------------
# This script is called from the phase 1 Makefile and updates all the
# .localmenu.*.xml files containing the local menus.
# -----------------------------------------------------------------------------
set -e
echo "* Updating local menus"
# -----------------------------------------------------------------------------
# Get a list of all source files containing local menus
# -----------------------------------------------------------------------------
all_files=$(
find . -name "*.xhtml" -not -name "*-template.*" \
| xargs grep -l "</localmenu>" \
| sort
)
# -----------------------------------------------------------------------------
# Split that list by localmenu directory
# -----------------------------------------------------------------------------
declare -A files_by_dir
for file in ${all_files}; do
dir=$(xsltproc build/xslt/get_localmenu_dir.xsl ${file})
dir=${dir:-$(dirname ${file})}
files_by_dir[${dir}]="${files_by_dir[${dir}]} ${file}"
done
# -----------------------------------------------------------------------------
# If any of the source files has been updated, rebuild all .localmenu.*.xml
# -----------------------------------------------------------------------------
for dir in ${!files_by_dir[@]}; do
for file in ${files_by_dir[${dir}]}; do
if [ "${file}" -nt "${dir}/.localmenu.en.xml" ]; then
# Find out which languages to generate.
languages="$1"
# Compile the list of base filenames of the source files.
basefiles=$(
ls ${files_by_dir[${dir}]} \
| sed 's/\...\.xhtml//' \
| sort --uniq
)
# For each language, create the .localmenu.${lang}.xml file.
for lang in $languages; do
echo "* Creating ${dir}/.localmenu.${lang}.xml"
{
echo "<?xml version=\"1.0\"?>"
echo ""
echo "<feed>"
for basefile in ${basefiles}; do
if [ -f "${basefile}.${lang}.xhtml" ]; then
file="${basefile}.${lang}.xhtml"
else
file="${basefile}.en.xhtml"
fi
xsltproc \
--stringparam "link" "$(echo "$basefile"| sed 's/^\.\/[^\/]*//').html" \
build/xslt/get_localmenu_line.xsl \
"${file}"
echo ""
done | sort
echo "</feed>"
} > "${dir}/.localmenu.${lang}.xml"
done
# The local menu for this directory has been built, no need to check
# further source files.
break
fi
done
done

View File

@ -1,56 +0,0 @@
#!/usr/bin/env bash
# -----------------------------------------------------------------------------
# Update XSL stylesheets (*.xsl) according to their dependency
# -----------------------------------------------------------------------------
# This script is called from the phase 1 Makefile and updates (actually: just
# touches) all XSL files which depend on another XSL file that has changed
# since the last build run. The phase 2 Makefile then only has to consider the
# directly used stylesheet as a prerequisite for building each file and doesn't
# have to worry about other stylesheets imported into that one.
# -----------------------------------------------------------------------------
set -e
set -o pipefail
echo "* Updating XSL stylesheets"
pid=$$
# -----------------------------------------------------------------------------
# Generate a temporary makefile with dependencies for all .xsl files
# -----------------------------------------------------------------------------
makefile="/tmp/makefile-${pid}"
{
for xsl_file in $(find * -name '*.xsl' -not -name '.default.xsl'); do
prerequisites=$(echo $(
cat "${xsl_file}" \
| tr '\t\r\n' ' ' \
| sed -r 's/(<xsl:(include|import)[^>]*>)/\n\1\n/g' \
| sed -rn '/<xsl:(include|import)[^>]*>/s/^.*href *= *"([^"]*)".*$/\1/gp' \
| xargs -I'{}' realpath --relative-to="." "$(dirname ${xsl_file})/{}" \
))
if [ -n "${prerequisites}" ]; then
echo "all: ${xsl_file}"
echo "${xsl_file}: ${prerequisites}"
echo ""
fi
done
echo '%.xsl:'
echo -e '\techo "* Touching $@"'
echo -e '\ttouch $@'
} > "${makefile}"
# -----------------------------------------------------------------------------
# Execute the temporary makefile
# -----------------------------------------------------------------------------
make --silent --file="${makefile}"
# -----------------------------------------------------------------------------
# Clean up
# -----------------------------------------------------------------------------
rm -f "${makefile}"

View File

@ -1,258 +0,0 @@
#!/usr/bin/env bash
# -----------------------------------------------------------------------------
# Update XML filelists (*.xmllist) and tag list pages (fsfe.org/tags/tagged-*)
# -----------------------------------------------------------------------------
# This script is called from the phase 1 Makefile and creates/updates the
# following files:
#
# * fsfe.org/tags/tagged-<tags>.en.xhtml for each tag used. Apart from being
# automatically created, these are regular source files for HTML pages, and
# in phase 2 are built into pages listing all news items and events for a
# tag.
#
# * fsfe.org/tags/.tags.??.xml with a list of the tags used.
#
# * <dir>/.<base>.xmllist for each <dir>/<base>.sources as well as for each
# fsfe.org/tags/tagged-<tags>.en.xhtml. These files are used in phase 2 to include the
# correct XML files when generating the HTML pages. It is taken care that
# these files are only updated whenever their content actually changes, so
# they can serve as a prerequisite in the phase 2 Makefile.
#
# Changing or removing tags in XML files is also considered, in which case a
# file is removed from the .xmllist files.
#
# When a tag has been removed from the last XML file where it has been used,
# the tagged-* are correctly deleted.
# -----------------------------------------------------------------------------
set -e
set -o pipefail
pid=$$
languages="$1"
nextyear=$(date --date="next year" +%Y)
thisyear=$(date --date="this year" +%Y)
lastyear=$(date --date="last year" +%Y)
# -----------------------------------------------------------------------------
# Make sure temporary directory is empty
# -----------------------------------------------------------------------------
tagmaps="/tmp/tagmaps-${pid}"
rm -rf "${tagmaps}"
mkdir "${tagmaps}"
taglabels="/tmp/taglabels-${pid}"
rm -rf "${taglabels}"
mkdir "${taglabels}"
# -----------------------------------------------------------------------------
# Create a complete and current map of which tag is used in which files
# -----------------------------------------------------------------------------
echo "* Generating tag maps"
for xml_file in $(find * -regex "[a-z.]+.[a-z]+/.*/*.\(${languages// /\\\|}\)\.xml" -not -path 'fsfe.org/tags/*' | xargs grep -l '<tag' | sort); do
xsltproc "build/xslt/get_tags.xsl" "${xml_file}" | while read tag label; do
# Add file to list of files by tag key
echo "${xml_file%.??.xml}" >> "${tagmaps}/${tag}"
# Store label by language and tag key
xml_base=${xml_file%.xml}
language=${xml_base##*.}
if [ "${language}" -a "${label}" ]; then
mkdir -p "${taglabels}/${language}"
# Always overwrite so the newest news item will win.
echo "${label}" > "${taglabels}/${language}/${tag}"
fi
done
done
# -----------------------------------------------------------------------------
# Make sure that each file only appears once per tag map
# -----------------------------------------------------------------------------
for tag in $(ls "${tagmaps}"); do
sort -u "${tagmaps}/${tag}" > "${tagmaps}/tmp"
mv "${tagmaps}/tmp" "${tagmaps}/${tag}"
done
# -----------------------------------------------------------------------------
# Update only those files where a change has happened
# -----------------------------------------------------------------------------
for tag in $(ls "${tagmaps}"); do
if ! cmp --quiet "${tagmaps}/${tag}" "fsfe.org/tags/.tagged-${tag}.xmllist"; then
echo "* Updating tag ${tag}"
sed "s,XXX_TAGNAME_XXX,${tag},g" "fsfe.org/tags/tagged.en.xhtml" \
> "fsfe.org/tags/tagged-${tag}.en.xhtml"
cp "${tagmaps}/${tag}" "fsfe.org/tags/.tagged-${tag}.xmllist"
fi
done
rm -f "fsfe.org/tags/tagged-front-page.en.xhtml" # We don't want that one
# -----------------------------------------------------------------------------
# Remove the files for tags which have been completely deleted
# -----------------------------------------------------------------------------
for tag in $(ls "fsfe.org/tags" | sed -rn 's/tagged-(.*)\.en.xhtml/\1/p'); do
if [ ! -f "${tagmaps}/${tag}" ]; then
echo "* Deleting fsfe.org/tags/tagged-${tag}.en.xhtml"
rm "fsfe.org/tags/tagged-${tag}.en.xhtml"
fi
done
for tag in $(ls -a "fsfe.org/tags" | sed -rn 's/.tagged-(.*)\.xmllist/\1/p'); do
if [ ! -f "${tagmaps}/${tag}" ]; then
echo "* Deleting fsfe.org/tags/.tagged-${tag}.xmllist"
rm "fsfe.org/tags/.tagged-${tag}.xmllist"
fi
done
# -----------------------------------------------------------------------------
# Update the tag lists
# -----------------------------------------------------------------------------
echo "* Updating tag lists"
taglist="/tmp/taglist-${pid}"
declare -A filecount
for section in "news" "events"; do
for tag in $(ls "${tagmaps}"); do
filecount["${tag}:${section}"]=$(grep "^fsfe.org/${section}/" "${tagmaps}/${tag}" | wc --lines || true)
done
done
for language in $(ls ${taglabels}); do
{
echo '<?xml version="1.0" encoding="UTF-8"?>'
echo ''
echo '<tagset>'
for section in "news" "events"; do
for tag in $(ls "${tagmaps}"); do
if [ "${tag}" == "front-page" ]; then
continue # We don't want an actual page for that
fi
count=${filecount["${tag}:${section}"]}
if [ -f "${taglabels}/${language}/${tag}" ]; then
label="$(cat "${taglabels}/${language}/${tag}")"
elif [ -f "${taglabels}/en/${tag}" ]; then
label="$(cat "${taglabels}/en/${tag}")"
else
label="${tag}"
fi
if [ "${count}" != "0" ]; then
echo " <tag section=\"${section}\" key=\"${tag}\" count=\"${count}\">${label}</tag>"
fi
done
done
echo '</tagset>'
} > ${taglist}
if ! cmp --quiet "${taglist}" "fsfe.org/tags/.tags.${language}.xml"; then
echo "* Updating fsfe.org/tags/.tags.${language}.xml"
cp "${taglist}" "fsfe.org/tags/.tags.${language}.xml"
fi
rm -f "${taglist}"
done
# -----------------------------------------------------------------------------
# Remove the temporary directory
# -----------------------------------------------------------------------------
rm -rf "${tagmaps}"
rm -rf "${taglabels}"
# -----------------------------------------------------------------------------
# Update .xmllist files for .sources and .xhtml containing <module>s
# -----------------------------------------------------------------------------
echo "* Updating XML lists"
all_xml="$(find * -name '*.??.xml' | sed -r 's/\...\.xml$//' | sort -u)"
source_bases="$(find * -name "*.sources" | sed -r 's/\.sources$//')"
module_bases="$(find * -name "*.??.xhtml" | xargs grep -l '<module' | sed -r 's/\...\.xhtml$//')"
all_bases="$((echo "${source_bases}"; echo "${module_bases}") | sort -u)"
for base in ${all_bases}; do
{
# Data files defined in the .sources list
if [ -f "${base}.sources" ]; then
cat "${base}.sources" | while read line; do
# Get the pattern from the pattern:[tag] construction
pattern=$(echo "${line}" | sed -rn 's/(.*):\[.*\]$/\1/p')
if [ -z "${pattern}" ]; then
continue
fi
# Honor $nextyear, $thisyear, and $lastyear variables
pattern=${pattern//\$nextyear/${nextyear}}
pattern=${pattern//\$thisyear/${thisyear}}
pattern=${pattern//\$lastyear/${lastyear}}
# Change from a glob pattern into a regex
pattern=$(echo "${pattern}" | sed -r -e 's/([.^$[])/\\\1/g; s/\*/.*/g')
# Get the tag from the pattern:[tag] construction
tag=$(echo "${line}" | sed -rn 's/.*:\[(.*)\]$/\1/p')
# We append || true so the script doesn't fail if grep finds nothing at all
if [ -n "${tag}" ]; then
cat "fsfe.org/tags/.tagged-${tag}.xmllist"
else
echo "${all_xml}"
fi | grep "^${pattern}\$" || true
done
fi
# Data files required for the <module>s used
for xhtml_file in ${base}.??.xhtml; do
xsltproc "build/xslt/get_modules.xsl" "${xhtml_file}"
done
} | sort -u > "/tmp/xmllist-${pid}"
list_file="$(dirname ${base})/.$(basename ${base}).xmllist"
if ! cmp --quiet "/tmp/xmllist-${pid}" "${list_file}"; then
echo "* Updating ${list_file}"
cp "/tmp/xmllist-${pid}" "${list_file}"
fi
rm -f "/tmp/xmllist-${pid}"
done
# -----------------------------------------------------------------------------
# Touch all .xmllist files where one of the contained files has changed
# -----------------------------------------------------------------------------
echo "* Checking contents of XML lists"
for list_file in $(find -name '.*.xmllist' -printf '%P\n' | sort); do
if [ ! -s "${list_file}" ]; then # Empty file list
continue
fi
xml_files="$(sed -r 's/(^.*$)/\1.??.xml/' "${list_file}")"
# For long file lists, the following would fail with an exit code of 141
# (SIGPIPE), so we must add a "|| true"
newest_xml_file="$(ls -t ${xml_files} | head --lines=1 || true)"
if [ "${newest_xml_file}" -nt "${list_file}" ]; then
echo "* Touching ${list_file}"
touch "${list_file}"
fi
done