From bc5b6ec2f1b7b71248c334073fbf0bca376d170f Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 13 Dec 2024 20:55:25 -0500 Subject: [PATCH 01/12] Remove Travis CI support --- .travis.yml | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b2a5f19..0000000 --- a/.travis.yml +++ /dev/null @@ -1,31 +0,0 @@ -language: generic -osx_image: xcode9.1 -os: - - linux - - osx -sudo: required -dist: trusty -addons: - apt: - packages: - - clang - - libfreetype6-dev - - libpng-dev - - pkg-config - - libcairo-dev -before_install: - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install cairo sdl2 lcms2 ; fi -install: - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then SWIFT_DIR=tests ; fi - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then mkdir $SWIFT_DIR ; fi - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then curl https://swift.org/builds/swift-4.0.2-release/ubuntu1404/swift-4.0.2-RELEASE/swift-4.0.2-RELEASE-ubuntu14.04.tar.gz -s | tar xz -C $SWIFT_DIR &> /dev/null ; fi - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get update ; fi - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install clang libcairo-dev libfreetype6-dev ; fi -env: - - SWIFT_VERSION=swift-4.0.2-RELEASE -script: - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then swift package generate-xcodeproj ; fi - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then xctool test -project Silica.xcodeproj -scheme "Silica" -sdk macosx ONLY_ACTIVE_ARCH=NO LIBRARY_SEARCH_PATHS=/usr/local/lib ; fi - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export PATH=$(pwd)/tests/$SWIFT_VERSION-ubuntu14.04/usr/bin:"${PATH}" ; fi - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then swift build ; fi - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then swift test ; fi \ No newline at end of file From 6bfa78f5bfd0908d38cd12ef40c433ab2c2fb0cd Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 13 Dec 2024 20:56:22 -0500 Subject: [PATCH 02/12] Ignore SwiftPM files --- .gitignore | 79 +++++++++++++++++++++++++++++++++++++++++++++--- Package.pins | 42 ------------------------- Package.resolved | 16 ---------- 3 files changed, 75 insertions(+), 62 deletions(-) delete mode 100644 Package.pins delete mode 100644 Package.resolved diff --git a/.gitignore b/.gitignore index 32e5bbc..e08d4a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,75 @@ -Carthage -.build -Packages -Silica.xcodeproj +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xccheckout +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +Packages/ +Package.pins +.build/ +Bluetooth.xcodeproj/* +.swiftpm + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ + +# Carthage +# + +Carthage/* + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output + +# Jazzy +docs + +# VS Code +.vscode + +# Finder +.DS_Store diff --git a/Package.pins b/Package.pins deleted file mode 100644 index d2dba36..0000000 --- a/Package.pins +++ /dev/null @@ -1,42 +0,0 @@ -{ - "autoPin": true, - "pins": [ - { - "package": "Cairo", - "reason": null, - "repositoryURL": "https://github.com/PureSwift/Cairo.git", - "version": "1.2.3" - }, - { - "package": "CCairo", - "reason": null, - "repositoryURL": "https://github.com/PureSwift/CCairo.git", - "version": "1.1.1" - }, - { - "package": "CFontConfig", - "reason": null, - "repositoryURL": "https://github.com/PureSwift/CFontConfig.git", - "version": "1.0.1" - }, - { - "package": "CFreeType", - "reason": null, - "repositoryURL": "https://github.com/PureSwift/CFreeType.git", - "version": "1.0.4" - }, - { - "package": "CLCMS", - "reason": null, - "repositoryURL": "https://github.com/PureSwift/CLCMS.git", - "version": "1.0.0" - }, - { - "package": "LittleCMS", - "reason": null, - "repositoryURL": "https://github.com/PureSwift/LittleCMS.git", - "version": "1.0.2" - } - ], - "version": 1 -} \ No newline at end of file diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index d84ced7..0000000 --- a/Package.resolved +++ /dev/null @@ -1,16 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "Cairo", - "repositoryURL": "https://github.com/PureSwift/Cairo.git", - "state": { - "branch": "master", - "revision": "b5f867a56a20d2f0064ccb975ae4a669b374e9e0", - "version": null - } - } - ] - }, - "version": 1 -} From 6f70e9d71432a82084acc9696ce90d1342998785 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 14 Dec 2024 03:22:06 +0000 Subject: [PATCH 03/12] Add VS Code container --- .devcontainer/Dockerfile | 25 + .devcontainer/devcontainer.json | 45 ++ .../library-scripts/common-debian.sh | 454 ++++++++++++++++++ .devcontainer/library-scripts/node-debian.sh | 170 +++++++ 4 files changed, 694 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/library-scripts/common-debian.sh create mode 100644 .devcontainer/library-scripts/node-debian.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..4cf3d1a --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,25 @@ +# [Choice] Swift version: +ARG VARIANT=6.0.3-jammy +FROM swift:${VARIANT} + +# [Option] Install zsh +ARG INSTALL_ZSH="true" +# [Option] Upgrade OS packages to their latest versions +ARG UPGRADE_PACKAGES="false" + +# Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies. +ARG USERNAME=vscode +ARG USER_UID=1000 +ARG USER_GID=$USER_UID +COPY library-scripts/common-debian.sh /tmp/library-scripts/ +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && /bin/bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" "true" "true" \ + && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* && rm -rf /tmp/library-scripts + +# Install Cairo +RUN export DEBIAN_FRONTEND=noninteractive \ + && apt-get update \ + && apt-get install -y libcairo2-dev \ + libfontconfig-dev \ + libfreetype6-dev \ + && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..94d7aff --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,45 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/swift +{ + "name": "Swift", + "build": { + "dockerfile": "Dockerfile", + "args": { + // Update the VARIANT arg to pick a Swift version + "VARIANT": "6.0.3-jammy", + // Options + "NODE_VERSION": "lts/*", + "UPGRADE_PACKAGES": "true" + } + }, + "runArgs": [ + "--cap-add=SYS_PTRACE", + "--security-opt", + "seccomp=unconfined" + ], + + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "lldb.library": "/usr/lib/liblldb.so" + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "sswg.swift-lang" + ] + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "", + + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} diff --git a/.devcontainer/library-scripts/common-debian.sh b/.devcontainer/library-scripts/common-debian.sh new file mode 100644 index 0000000..efdca35 --- /dev/null +++ b/.devcontainer/library-scripts/common-debian.sh @@ -0,0 +1,454 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/common.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] [install Oh My Zsh! flag] [Add non-free packages] + +set -e + +INSTALL_ZSH=${1:-"true"} +USERNAME=${2:-"automatic"} +USER_UID=${3:-"automatic"} +USER_GID=${4:-"automatic"} +UPGRADE_PACKAGES=${5:-"true"} +INSTALL_OH_MYS=${6:-"true"} +ADD_NON_FREE_PACKAGES=${7:-"false"} +SCRIPT_DIR="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)" +MARKER_FILE="/usr/local/etc/vscode-dev-containers/common" + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Ensure that login shells get the correct path if the user updated the PATH using ENV. +rm -f /etc/profile.d/00-restore-env.sh +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh +chmod +x /etc/profile.d/00-restore-env.sh + +# If in automatic mode, determine if a user already exists, if not use vscode +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=vscode + fi +elif [ "${USERNAME}" = "none" ]; then + USERNAME=root + USER_UID=0 + USER_GID=0 +fi + +# Load markers to see which steps have already run +if [ -f "${MARKER_FILE}" ]; then + echo "Marker file found:" + cat "${MARKER_FILE}" + source "${MARKER_FILE}" +fi + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +# Function to call apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies +if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then + + package_list="apt-utils \ + openssh-client \ + gnupg2 \ + dirmngr \ + iproute2 \ + procps \ + lsof \ + htop \ + net-tools \ + psmisc \ + curl \ + wget \ + rsync \ + ca-certificates \ + unzip \ + zip \ + nano \ + vim-tiny \ + less \ + jq \ + lsb-release \ + apt-transport-https \ + dialog \ + libc6 \ + libgcc1 \ + libkrb5-3 \ + libgssapi-krb5-2 \ + libicu[0-9][0-9] \ + liblttng-ust[0-9] \ + libstdc++6 \ + zlib1g \ + locales \ + sudo \ + ncdu \ + man-db \ + strace \ + manpages \ + manpages-dev \ + init-system-helpers" + + # Needed for adding manpages-posix and manpages-posix-dev which are non-free packages in Debian + if [ "${ADD_NON_FREE_PACKAGES}" = "true" ]; then + # Bring in variables from /etc/os-release like VERSION_CODENAME + . /etc/os-release + sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list + sed -i -E "s/deb-src http:\/\/(deb|httredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list + sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list + sed -i -E "s/deb-src http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb-src http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list + # Handle bullseye location for security https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html + sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list + echo "Running apt-get update..." + apt-get update + package_list="${package_list} manpages-posix manpages-posix-dev" + else + apt_get_update_if_needed + fi + + # Install libssl1.1 if available + if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then + package_list="${package_list} libssl1.1" + fi + + # Install appropriate version of libssl1.0.x if available + libssl_package=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '') + if [ "$(echo "$LIlibssl_packageBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then + if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then + # Debian 9 + package_list="${package_list} libssl1.0.2" + elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then + # Ubuntu 18.04, 16.04, earlier + package_list="${package_list} libssl1.0.0" + fi + fi + + echo "Packages to verify are installed: ${package_list}" + apt-get -y install --no-install-recommends ${package_list} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 ) + + # Install git if not already installed (may be more recent than distro version) + if ! type git > /dev/null 2>&1; then + apt-get -y install --no-install-recommends git + fi + + PACKAGES_ALREADY_INSTALLED="true" +fi + +# Get to latest versions of all packages +if [ "${UPGRADE_PACKAGES}" = "true" ]; then + apt_get_update_if_needed + apt-get -y upgrade --no-install-recommends + apt-get autoremove -y +fi + +# Ensure at least the en_US.UTF-8 UTF-8 locale is available. +# Common need for both applications and things like the agnoster ZSH theme. +if [ "${LOCALE_ALREADY_SET}" != "true" ] && ! grep -o -E '^\s*en_US.UTF-8\s+UTF-8' /etc/locale.gen > /dev/null; then + echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen + locale-gen + LOCALE_ALREADY_SET="true" +fi + +# Create or update a non-root user to match UID/GID. +group_name="${USERNAME}" +if id -u ${USERNAME} > /dev/null 2>&1; then + # User exists, update if needed + if [ "${USER_GID}" != "automatic" ] && [ "$USER_GID" != "$(id -g $USERNAME)" ]; then + group_name="$(id -gn $USERNAME)" + groupmod --gid $USER_GID ${group_name} + usermod --gid $USER_GID $USERNAME + fi + if [ "${USER_UID}" != "automatic" ] && [ "$USER_UID" != "$(id -u $USERNAME)" ]; then + usermod --uid $USER_UID $USERNAME + fi +else + # Create user + if [ "${USER_GID}" = "automatic" ]; then + groupadd $USERNAME + else + groupadd --gid $USER_GID $USERNAME + fi + if [ "${USER_UID}" = "automatic" ]; then + useradd -s /bin/bash --gid $USERNAME -m $USERNAME + else + useradd -s /bin/bash --uid $USER_UID --gid $USERNAME -m $USERNAME + fi +fi + +# Add sudo support for non-root user +if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then + echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME + chmod 0440 /etc/sudoers.d/$USERNAME + EXISTING_NON_ROOT_USER="${USERNAME}" +fi + +# ** Shell customization section ** +if [ "${USERNAME}" = "root" ]; then + user_rc_path="/root" +else + user_rc_path="/home/${USERNAME}" +fi + +# Restore user .bashrc defaults from skeleton file if it doesn't exist or is empty +if [ ! -f "${user_rc_path}/.bashrc" ] || [ ! -s "${user_rc_path}/.bashrc" ] ; then + cp /etc/skel/.bashrc "${user_rc_path}/.bashrc" +fi + +# Restore user .profile defaults from skeleton file if it doesn't exist or is empty +if [ ! -f "${user_rc_path}/.profile" ] || [ ! -s "${user_rc_path}/.profile" ] ; then + cp /etc/skel/.profile "${user_rc_path}/.profile" +fi + +# .bashrc/.zshrc snippet +rc_snippet="$(cat << 'EOF' + +if [ -z "${USER}" ]; then export USER=$(whoami); fi +if [[ "${PATH}" != *"$HOME/.local/bin"* ]]; then export PATH="${PATH}:$HOME/.local/bin"; fi + +# Display optional first run image specific notice if configured and terminal is interactive +if [ -t 1 ] && [[ "${TERM_PROGRAM}" = "vscode" || "${TERM_PROGRAM}" = "codespaces" ]] && [ ! -f "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed" ]; then + if [ -f "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" ]; then + cat "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" + elif [ -f "/workspaces/.codespaces/shared/first-run-notice.txt" ]; then + cat "/workspaces/.codespaces/shared/first-run-notice.txt" + fi + mkdir -p "$HOME/.config/vscode-dev-containers" + # Mark first run notice as displayed after 10s to avoid problems with fast terminal refreshes hiding it + ((sleep 10s; touch "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed") &) +fi + +# Set the default git editor if not already set +if [ -z "$(git config --get core.editor)" ] && [ -z "${GIT_EDITOR}" ]; then + if [ "${TERM_PROGRAM}" = "vscode" ]; then + if [[ -n $(command -v code-insiders) && -z $(command -v code) ]]; then + export GIT_EDITOR="code-insiders --wait" + else + export GIT_EDITOR="code --wait" + fi + fi +fi + +EOF +)" + +# code shim, it fallbacks to code-insiders if code is not available +cat << 'EOF' > /usr/local/bin/code +#!/bin/sh + +get_in_path_except_current() { + which -a "$1" | grep -A1 "$0" | grep -v "$0" +} + +code="$(get_in_path_except_current code)" + +if [ -n "$code" ]; then + exec "$code" "$@" +elif [ "$(command -v code-insiders)" ]; then + exec code-insiders "$@" +else + echo "code or code-insiders is not installed" >&2 + exit 127 +fi +EOF +chmod +x /usr/local/bin/code + +# systemctl shim - tells people to use 'service' if systemd is not running +cat << 'EOF' > /usr/local/bin/systemctl +#!/bin/sh +set -e +if [ -d "/run/systemd/system" ]; then + exec /bin/systemctl "$@" +else + echo '\n"systemd" is not running in this container due to its overhead.\nUse the "service" command to start services instead. e.g.: \n\nservice --status-all' +fi +EOF +chmod +x /usr/local/bin/systemctl + +# Codespaces bash and OMZ themes - partly inspired by https://github.com/ohmyzsh/ohmyzsh/blob/master/themes/robbyrussell.zsh-theme +codespaces_bash="$(cat \ +<<'EOF' + +# Codespaces bash prompt theme +__bash_prompt() { + local userpart='`export XIT=$? \ + && [ ! -z "${GITHUB_USER}" ] && echo -n "\[\033[0;32m\]@${GITHUB_USER} " || echo -n "\[\033[0;32m\]\u " \ + && [ "$XIT" -ne "0" ] && echo -n "\[\033[1;31m\]➜" || echo -n "\[\033[0m\]➜"`' + local gitbranch='`\ + if [ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ]; then \ + export BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null); \ + if [ "${BRANCH}" != "" ]; then \ + echo -n "\[\033[0;36m\](\[\033[1;31m\]${BRANCH}" \ + && if git ls-files --error-unmatch -m --directory --no-empty-directory -o --exclude-standard ":/*" > /dev/null 2>&1; then \ + echo -n " \[\033[1;33m\]✗"; \ + fi \ + && echo -n "\[\033[0;36m\]) "; \ + fi; \ + fi`' + local lightblue='\[\033[1;34m\]' + local removecolor='\[\033[0m\]' + PS1="${userpart} ${lightblue}\w ${gitbranch}${removecolor}\$ " + unset -f __bash_prompt +} +__bash_prompt + +EOF +)" + +codespaces_zsh="$(cat \ +<<'EOF' +# Codespaces zsh prompt theme +__zsh_prompt() { + local prompt_username + if [ ! -z "${GITHUB_USER}" ]; then + prompt_username="@${GITHUB_USER}" + else + prompt_username="%n" + fi + PROMPT="%{$fg[green]%}${prompt_username} %(?:%{$reset_color%}➜ :%{$fg_bold[red]%}➜ )" # User/exit code arrow + PROMPT+='%{$fg_bold[blue]%}%(5~|%-1~/…/%3~|%4~)%{$reset_color%} ' # cwd + PROMPT+='$([ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ] && git_prompt_info)' # Git status + PROMPT+='%{$fg[white]%}$ %{$reset_color%}' + unset -f __zsh_prompt +} +ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[cyan]%}(%{$fg_bold[red]%}" +ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} " +ZSH_THEME_GIT_PROMPT_DIRTY=" %{$fg_bold[yellow]%}✗%{$fg_bold[cyan]%})" +ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg_bold[cyan]%})" +__zsh_prompt + +EOF +)" + +# Add RC snippet and custom bash prompt +if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then + echo "${rc_snippet}" >> /etc/bash.bashrc + echo "${codespaces_bash}" >> "${user_rc_path}/.bashrc" + echo 'export PROMPT_DIRTRIM=4' >> "${user_rc_path}/.bashrc" + if [ "${USERNAME}" != "root" ]; then + echo "${codespaces_bash}" >> "/root/.bashrc" + echo 'export PROMPT_DIRTRIM=4' >> "/root/.bashrc" + fi + chown ${USERNAME}:${group_name} "${user_rc_path}/.bashrc" + RC_SNIPPET_ALREADY_ADDED="true" +fi + +# Optionally install and configure zsh and Oh My Zsh! +if [ "${INSTALL_ZSH}" = "true" ]; then + if ! type zsh > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get install -y zsh + fi + if [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then + echo "${rc_snippet}" >> /etc/zsh/zshrc + ZSH_ALREADY_INSTALLED="true" + fi + + # Adapted, simplified inline Oh My Zsh! install steps that adds, defaults to a codespaces theme. + # See https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/install.sh for official script. + oh_my_install_dir="${user_rc_path}/.oh-my-zsh" + if [ ! -d "${oh_my_install_dir}" ] && [ "${INSTALL_OH_MYS}" = "true" ]; then + template_path="${oh_my_install_dir}/templates/zshrc.zsh-template" + user_rc_file="${user_rc_path}/.zshrc" + umask g-w,o-w + mkdir -p ${oh_my_install_dir} + git clone --depth=1 \ + -c core.eol=lf \ + -c core.autocrlf=false \ + -c fsck.zeroPaddedFilemode=ignore \ + -c fetch.fsck.zeroPaddedFilemode=ignore \ + -c receive.fsck.zeroPaddedFilemode=ignore \ + "https://github.com/ohmyzsh/ohmyzsh" "${oh_my_install_dir}" 2>&1 + echo -e "$(cat "${template_path}")\nDISABLE_AUTO_UPDATE=true\nDISABLE_UPDATE_PROMPT=true" > ${user_rc_file} + sed -i -e 's/ZSH_THEME=.*/ZSH_THEME="codespaces"/g' ${user_rc_file} + + mkdir -p ${oh_my_install_dir}/custom/themes + echo "${codespaces_zsh}" > "${oh_my_install_dir}/custom/themes/codespaces.zsh-theme" + # Shrink git while still enabling updates + cd "${oh_my_install_dir}" + git repack -a -d -f --depth=1 --window=1 + # Copy to non-root user if one is specified + if [ "${USERNAME}" != "root" ]; then + cp -rf "${user_rc_file}" "${oh_my_install_dir}" /root + chown -R ${USERNAME}:${group_name} "${user_rc_path}" + fi + fi +fi + +# Persist image metadata info, script if meta.env found in same directory +meta_info_script="$(cat << 'EOF' +#!/bin/sh +. /usr/local/etc/vscode-dev-containers/meta.env + +# Minimal output +if [ "$1" = "version" ] || [ "$1" = "image-version" ]; then + echo "${VERSION}" + exit 0 +elif [ "$1" = "release" ]; then + echo "${GIT_REPOSITORY_RELEASE}" + exit 0 +elif [ "$1" = "content" ] || [ "$1" = "content-url" ] || [ "$1" = "contents" ] || [ "$1" = "contents-url" ]; then + echo "${CONTENTS_URL}" + exit 0 +fi + +#Full output +echo +echo "Development container image information" +echo +if [ ! -z "${VERSION}" ]; then echo "- Image version: ${VERSION}"; fi +if [ ! -z "${DEFINITION_ID}" ]; then echo "- Definition ID: ${DEFINITION_ID}"; fi +if [ ! -z "${VARIANT}" ]; then echo "- Variant: ${VARIANT}"; fi +if [ ! -z "${GIT_REPOSITORY}" ]; then echo "- Source code repository: ${GIT_REPOSITORY}"; fi +if [ ! -z "${GIT_REPOSITORY_RELEASE}" ]; then echo "- Source code release/branch: ${GIT_REPOSITORY_RELEASE}"; fi +if [ ! -z "${BUILD_TIMESTAMP}" ]; then echo "- Timestamp: ${BUILD_TIMESTAMP}"; fi +if [ ! -z "${CONTENTS_URL}" ]; then echo && echo "More info: ${CONTENTS_URL}"; fi +echo +EOF +)" +if [ -f "${SCRIPT_DIR}/meta.env" ]; then + mkdir -p /usr/local/etc/vscode-dev-containers/ + cp -f "${SCRIPT_DIR}/meta.env" /usr/local/etc/vscode-dev-containers/meta.env + echo "${meta_info_script}" > /usr/local/bin/devcontainer-info + chmod +x /usr/local/bin/devcontainer-info +fi + +# Write marker file +mkdir -p "$(dirname "${MARKER_FILE}")" +echo -e "\ + PACKAGES_ALREADY_INSTALLED=${PACKAGES_ALREADY_INSTALLED}\n\ + LOCALE_ALREADY_SET=${LOCALE_ALREADY_SET}\n\ + EXISTING_NON_ROOT_USER=${EXISTING_NON_ROOT_USER}\n\ + RC_SNIPPET_ALREADY_ADDED=${RC_SNIPPET_ALREADY_ADDED}\n\ + ZSH_ALREADY_INSTALLED=${ZSH_ALREADY_INSTALLED}" > "${MARKER_FILE}" + +echo "Done!" diff --git a/.devcontainer/library-scripts/node-debian.sh b/.devcontainer/library-scripts/node-debian.sh new file mode 100644 index 0000000..f782961 --- /dev/null +++ b/.devcontainer/library-scripts/node-debian.sh @@ -0,0 +1,170 @@ +#!/bin/bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/node.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./node-debian.sh [directory to install nvm] [node version to install (use "none" to skip)] [non-root user] [Update rc files flag] [install node-gyp deps] + +export NVM_DIR=${1:-"/usr/local/share/nvm"} +export NODE_VERSION=${2:-"lts"} +USERNAME=${3:-"automatic"} +UPDATE_RC=${4:-"true"} +INSTALL_TOOLS_FOR_NODE_GYP="${5:-true}" +export NVM_VERSION="0.38.0" + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Ensure that login shells get the correct path if the user updated the PATH using ENV. +rm -f /etc/profile.d/00-restore-env.sh +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh +chmod +x /etc/profile.d/00-restore-env.sh + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +updaterc() { + if [ "${UPDATE_RC}" = "true" ]; then + echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." + if [[ "$(cat /etc/bash.bashrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/bash.bashrc + fi + if [ -f "/etc/zsh/zshrc" ] && [[ "$(cat /etc/zsh/zshrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/zsh/zshrc + fi + fi +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +# Install dependencies +check_packages apt-transport-https curl ca-certificates tar gnupg2 dirmngr + +# Install yarn +if type yarn > /dev/null 2>&1; then + echo "Yarn already installed." +else + # Import key safely (new method rather than deprecated apt-key approach) and install + curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor > /usr/share/keyrings/yarn-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/yarn-archive-keyring.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list + apt-get update + apt-get -y install --no-install-recommends yarn +fi + +# Adjust node version if required +if [ "${NODE_VERSION}" = "none" ]; then + export NODE_VERSION= +elif [ "${NODE_VERSION}" = "lts" ]; then + export NODE_VERSION="lts/*" +fi + +# Create a symlink to the installed version for use in Dockerfile PATH statements +export NVM_SYMLINK_CURRENT=true + +# Install the specified node version if NVM directory already exists, then exit +if [ -d "${NVM_DIR}" ]; then + echo "NVM already installed." + if [ "${NODE_VERSION}" != "" ]; then + su ${USERNAME} -c ". $NVM_DIR/nvm.sh && nvm install ${NODE_VERSION} && nvm clear-cache" + fi + exit 0 +fi + +# Create nvm group, nvm dir, and set sticky bit +if ! cat /etc/group | grep -e "^nvm:" > /dev/null 2>&1; then + groupadd -r nvm +fi +umask 0002 +usermod -a -G nvm ${USERNAME} +mkdir -p ${NVM_DIR} +chown :nvm ${NVM_DIR} +chmod g+s ${NVM_DIR} +su ${USERNAME} -c "$(cat << EOF + set -e + umask 0002 + # Do not update profile - we'll do this manually + export PROFILE=/dev/null + ls -lah /home/${USERNAME}/.nvs || : + curl -so- https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh | bash + source ${NVM_DIR}/nvm.sh + if [ "${NODE_VERSION}" != "" ]; then + nvm alias default ${NODE_VERSION} + fi + nvm clear-cache +EOF +)" 2>&1 +# Update rc files +if [ "${UPDATE_RC}" = "true" ]; then +updaterc "$(cat < /dev/null 2>&1; then + to_install="${to_install} make" + fi + if ! type gcc > /dev/null 2>&1; then + to_install="${to_install} gcc" + fi + if ! type g++ > /dev/null 2>&1; then + to_install="${to_install} g++" + fi + if ! type python3 > /dev/null 2>&1; then + to_install="${to_install} python3-minimal" + fi + if [ ! -z "${to_install}" ]; then + apt_get_update_if_needed + apt-get -y install ${to_install} + fi +fi + +echo "Done!" From 790dc5aa22ccaf6f53bb78408b67545ddad98b16 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 14 Dec 2024 03:22:33 +0000 Subject: [PATCH 04/12] Update for Swift 6 --- Package.swift | 4 +- Sources/Silica/CGAffineTransform.swift | 2 +- Sources/Silica/CGBitmapInfo.swift | 4 +- Sources/Silica/CGColor.swift | 12 +- Sources/Silica/CGContext.swift | 38 +- Sources/Silica/CGDrawingMode.swift | 11 +- Sources/Silica/CGFont.swift | 41 +- Sources/Silica/CGImageDestination.swift | 2 +- Sources/Silica/CGImageSource.swift | 56 +- Sources/Silica/CGImageSourcePNG.swift | 10 +- .../Silica/UIKit}/UIBezierPath.swift | 5 +- .../Silica/UIKit}/UIColor.swift | 14 +- .../Silica/UIKit}/UIGraphics.swift | 16 +- .../Silica/UIKit}/UIImage.swift | 4 +- Sources/Silica/UIKit/UIRectCorner.swift | 28 + Tests/LinuxMain.swift | 16 - Tests/SilicaTests/FontTests.swift | 6 +- Tests/SilicaTests/StyleKitTests.swift | 12 +- .../Utilities/Cacao/UIRectCorner.swift | 22 - Tests/SilicaTests/Utilities/Define.swift | 18 +- Tests/SilicaTests/Utilities/HTTP/HTTP.swift | 19 - .../Utilities/HTTP/HTTPClient.swift | 139 ----- .../Utilities/HTTP/HTTPMethod.swift | 27 - .../Utilities/HTTP/HTTPRequest.swift | 44 -- .../Utilities/HTTP/HTTPResponse.swift | 43 -- .../Utilities/HTTP/HTTPStatusCode.swift | 517 ------------------ .../Utilities/HTTP/HTTPVersion.swift | 35 -- .../Utilities/HTTP/URLClient.swift | 16 - .../Utilities/HTTP/URLProtocol.swift | 15 - .../Utilities/HTTP/URLRequest.swift | 17 - .../Utilities/HTTP/URLResponse.swift | 14 - Tests/SilicaTests/Utilities/TestAssets.swift | 79 ++- .../SilicaTests/Utilities/TestStyleKit.swift | 6 +- Tests/SilicaTests/Utilities/URLClient.swift | 48 ++ 34 files changed, 207 insertions(+), 1133 deletions(-) rename {Tests/SilicaTests/Utilities/Cacao => Sources/Silica/UIKit}/UIBezierPath.swift (99%) rename {Tests/SilicaTests/Utilities/Cacao => Sources/Silica/UIKit}/UIColor.swift (85%) rename {Tests/SilicaTests/Utilities/Cacao => Sources/Silica/UIKit}/UIGraphics.swift (81%) rename {Tests/SilicaTests/Utilities/Cacao => Sources/Silica/UIKit}/UIImage.swift (90%) create mode 100644 Sources/Silica/UIKit/UIRectCorner.swift delete mode 100644 Tests/LinuxMain.swift delete mode 100644 Tests/SilicaTests/Utilities/Cacao/UIRectCorner.swift delete mode 100755 Tests/SilicaTests/Utilities/HTTP/HTTP.swift delete mode 100644 Tests/SilicaTests/Utilities/HTTP/HTTPClient.swift delete mode 100755 Tests/SilicaTests/Utilities/HTTP/HTTPMethod.swift delete mode 100755 Tests/SilicaTests/Utilities/HTTP/HTTPRequest.swift delete mode 100755 Tests/SilicaTests/Utilities/HTTP/HTTPResponse.swift delete mode 100755 Tests/SilicaTests/Utilities/HTTP/HTTPStatusCode.swift delete mode 100755 Tests/SilicaTests/Utilities/HTTP/HTTPVersion.swift delete mode 100755 Tests/SilicaTests/Utilities/HTTP/URLClient.swift delete mode 100755 Tests/SilicaTests/Utilities/HTTP/URLProtocol.swift delete mode 100755 Tests/SilicaTests/Utilities/HTTP/URLRequest.swift delete mode 100755 Tests/SilicaTests/Utilities/HTTP/URLResponse.swift create mode 100644 Tests/SilicaTests/Utilities/URLClient.swift diff --git a/Package.swift b/Package.swift index 13255d4..df334d0 100644 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,4 @@ -// swift-tools-version:5.1 -// The swift-tools-version declares the minimum version of Swift required to build this package. - +// swift-tools-version:6.0 import PackageDescription let package = Package( diff --git a/Sources/Silica/CGAffineTransform.swift b/Sources/Silica/CGAffineTransform.swift index 9ad9a43..2f3be96 100644 --- a/Sources/Silica/CGAffineTransform.swift +++ b/Sources/Silica/CGAffineTransform.swift @@ -36,7 +36,7 @@ public struct CGAffineTransform { self.ty = ty } - public static let identity = CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0) + public static var identity: CGAffineTransform { CGAffineTransform(a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0) } } #endif diff --git a/Sources/Silica/CGBitmapInfo.swift b/Sources/Silica/CGBitmapInfo.swift index 06730da..97ca677 100644 --- a/Sources/Silica/CGBitmapInfo.swift +++ b/Sources/Silica/CGBitmapInfo.swift @@ -61,7 +61,7 @@ public extension CGBitmapInfo { /// /// - Note: Silica supports premultiplied alpha only for images. /// You should not premultiply any other color values specified in Silica. - public enum Alpha { + enum Alpha { /// There is no alpha channel. case none @@ -93,7 +93,7 @@ public extension CGBitmapInfo { } /// The byte ordering of pixel formats. - public enum ByteOrder { + enum ByteOrder { /// The default byte order. case `default` diff --git a/Sources/Silica/CGColor.swift b/Sources/Silica/CGColor.swift index 41a404d..56c501e 100644 --- a/Sources/Silica/CGColor.swift +++ b/Sources/Silica/CGColor.swift @@ -41,17 +41,17 @@ public struct CGColor: Equatable { // MARK: - Singletons - public static let clear = CGColor(red: 0, green: 0, blue: 0, alpha: 0) + public static var clear: CGColor { CGColor(red: 0, green: 0, blue: 0, alpha: 0) } - public static let black = CGColor(red: 0, green: 0, blue: 0) + public static var black: CGColor { CGColor(red: 0, green: 0, blue: 0) } - public static let white = CGColor(red: 1, green: 1, blue: 1) + public static var white: CGColor { CGColor(red: 1, green: 1, blue: 1) } - public static let red = CGColor(red: 1, green: 0, blue: 0) + public static var red: CGColor { CGColor(red: 1, green: 0, blue: 0) } - public static let green = CGColor(red: 0, green: 1, blue: 0) + public static var green: CGColor { CGColor(red: 0, green: 1, blue: 0) } - public static let blue = CGColor(red: 0, green: 0, blue: 1) + public static var blue: CGColor { CGColor(red: 0, green: 0, blue: 1) } } // MARK: - Equatable diff --git a/Sources/Silica/CGContext.swift b/Sources/Silica/CGContext.swift index e27a2a6..40c997a 100644 --- a/Sources/Silica/CGContext.swift +++ b/Sources/Silica/CGContext.swift @@ -527,7 +527,7 @@ public final class CGContext { startShadow() } - internalContext.source = internalState.stroke?.pattern ?? DefaultPattern + internalContext.source = internalState.stroke?.pattern ?? .default internalContext.stroke() @@ -552,7 +552,7 @@ public final class CGContext { public func clear() { - internalContext.source = internalState.fill?.pattern ?? DefaultPattern + internalContext.source = internalState.fill?.pattern ?? .default internalContext.clip() internalContext.clipPreserve() @@ -698,7 +698,7 @@ public final class CGContext { internalContext.setFont(matrix: cairoTextMatrix) - internalContext.source = internalState.fill?.pattern ?? DefaultPattern + internalContext.source = internalState.fill?.pattern ?? .default internalContext.show(text: text) @@ -797,7 +797,7 @@ public final class CGContext { internalContext.setFont(matrix: cairoTextMatrix) - internalContext.source = internalState.fill?.pattern ?? DefaultPattern + internalContext.source = internalState.fill?.pattern ?? .default // show glyphs cairoGlyphs.forEach { internalContext.show(glyph: $0) } @@ -812,7 +812,7 @@ public final class CGContext { startShadow() } - internalContext.source = internalState.fill?.pattern ?? DefaultPattern + internalContext.source = internalState.fill?.pattern ?? .default internalContext.fillRule = evenOdd ? CAIRO_FILL_RULE_EVEN_ODD : CAIRO_FILL_RULE_WINDING @@ -876,9 +876,12 @@ public final class CGContext { // MARK: - Private /// Default black pattern -fileprivate let DefaultPattern = Cairo.Pattern(color: (red: 0, green: 0, blue: 0)) +internal extension Cairo.Pattern { + + nonisolated(unsafe) static var `default`: Cairo.Pattern = Cairo.Pattern(color: (red: 0, green: 0, blue: 0)) +} -fileprivate extension Silica.CGContext { +internal extension Silica.CGContext { /// To save non-Cairo state variables fileprivate final class State { @@ -920,7 +923,7 @@ internal extension Collection { func indexedMap(_ transform: (Index, Iterator.Element) throws -> T) rethrows -> [T] { - let count: Int = numericCast(self.count) + let count = self.count if count == 0 { return [] } @@ -942,7 +945,7 @@ internal extension Collection { @inline(__always) func merge (_ other: C) -> [(Iterator.Element, T)] - where C.Iterator.Element == T, C.IndexDistance == IndexDistance, C.Index == Index { + where C.Iterator.Element == T, C.Index == Index { precondition(self.count == other.count, "The collection to merge must be of the same size") @@ -952,16 +955,15 @@ internal extension Collection { #if os(macOS) && Xcode - import Foundation - import AppKit +import Foundation +import AppKit - public extension Silica.CGContext { - - @objc(debugQuickLookObject) - public var debugQuickLookObject: AnyObject { - - return surface.debugQuickLookObject - } +public extension Silica.CGContext { + + @objc(debugQuickLookObject) + var debugQuickLookObject: AnyObject { + return surface.debugQuickLookObject } +} #endif diff --git a/Sources/Silica/CGDrawingMode.swift b/Sources/Silica/CGDrawingMode.swift index f9b7bdc..bd403d4 100644 --- a/Sources/Silica/CGDrawingMode.swift +++ b/Sources/Silica/CGDrawingMode.swift @@ -7,7 +7,7 @@ // /// Options for rendering text. -public enum CGTextDrawingMode: CInt { +public enum CGTextDrawingMode: CInt, Sendable { case fill case stroke @@ -30,7 +30,6 @@ public enum CGDrawingMode { /// Render the area within the path using the even-odd rule. case evenOddFill - public static let eoFill = CGDrawingMode.evenOddFill /// Render a line along the path. case stroke @@ -41,7 +40,13 @@ public enum CGDrawingMode { /// First fill and then stroke the path, using the even-odd rule. case evenOddFillStroke - public static let eoFillStroke = CGDrawingMode.evenOddFillStroke + // Source compatibility + + /// kCGPathEOFill + public static var eoFill: CGDrawingMode { .evenOddFill } + + /// kCGPathEOFillStroke + public static var eoFillStroke: CGDrawingMode { .evenOddFillStroke } public init() { self = .fill } } diff --git a/Sources/Silica/CGFont.swift b/Sources/Silica/CGFont.swift index cb6a1a3..7407982 100644 --- a/Sources/Silica/CGFont.swift +++ b/Sources/Silica/CGFont.swift @@ -9,14 +9,16 @@ import Cairo import CCairo import CFontConfig - import Foundation +#if os(macOS) +import struct CoreGraphics.CGAffineTransform +#endif /// Silica's `Font` type. -public struct CGFont: Equatable, Hashable { +public struct CGFont { /// Private font cache. - private static var cache = [String: CGFont]() + internal nonisolated(unsafe) static var cache = [String: CGFont]() // MARK: - Properties @@ -68,22 +70,29 @@ public struct CGFont: Equatable, Hashable { // MARK: - Equatable -public func == (lhs: CGFont, rhs: CGFont) -> Bool { +extension CGFont: Equatable { - // quick and easy way - return lhs.name == rhs.name + public static func == (lhs: CGFont, rhs: CGFont) -> Bool { + lhs.name == rhs.name + } } // MARK: - Hashable -public extension CGFont { +extension CGFont: Hashable { - var hashValue: Int { - - return name.hashValue + public func hash(into hasher: inout Hasher) { + name.hash(into: &hasher) } } +// MARK: - Identifiable + +extension CGFont: Identifiable { + + public var id: String { name } +} + // MARK: - Text Math public extension CGFont { @@ -127,7 +136,7 @@ public extension CGFont { // MARK: - Private /// Initialize a pointer to a `FcPattern` object created from the specified PostScript font name. -private func FcPattern(name: String) -> (pointer: OpaquePointer, family: String)? { +internal func FcPattern(name: String) -> (pointer: OpaquePointer, family: String)? { guard let pattern = FcPatternCreate() else { return nil } @@ -232,17 +241,7 @@ internal extension String { func substring(range: Range) -> String? { let indexRange = utf8.index(utf8.startIndex, offsetBy: range.lowerBound) ..< utf8.index(utf8.startIndex, offsetBy: range.upperBound) - let substring = String(utf8[indexRange]) - return substring } } - -internal extension String { - - func contains(_ other: String) -> Bool { - - return strstr(self, other) != nil - } -} diff --git a/Sources/Silica/CGImageDestination.swift b/Sources/Silica/CGImageDestination.swift index 58a489e..f4667a2 100644 --- a/Sources/Silica/CGImageDestination.swift +++ b/Sources/Silica/CGImageDestination.swift @@ -10,7 +10,7 @@ import struct Foundation.Data /// This object abstracts the data-writing task. /// An image source can write image data to `Data`. -public protocol ImageDestination: class, RandomAccessCollection, MutableCollection { +public protocol ImageDestination: AnyObject, RandomAccessCollection, MutableCollection { } diff --git a/Sources/Silica/CGImageSource.swift b/Sources/Silica/CGImageSource.swift index 407794f..9c5daac 100644 --- a/Sources/Silica/CGImageSource.swift +++ b/Sources/Silica/CGImageSource.swift @@ -10,11 +10,7 @@ import struct Foundation.Data /// This object abstracts the data-reading task. /// An image source can read image data from a `Data` instance. -public protocol CGImageSource: class, RandomAccessCollection { - - associatedtype Index = Int - associatedtype Indices = DefaultIndices - associatedtype Iterator = IndexingIterator +public protocol CGImageSource: AnyObject { static var typeIdentifier: String { get } @@ -23,50 +19,6 @@ public protocol CGImageSource: class, RandomAccessCollection { func createImage(at index: Int) -> CGImage? } -public extension CGImageSource { - - public var count: Int { return 1 } // only some formats like GIF have multiple images - - public subscript (index: Int) -> CGImage { - - guard let image = createImage(at: index) - else { fatalError("No image at index \(index)") } - - return image - } - - public subscript(bounds: Range) -> Slice { - - return Slice(base: self, bounds: bounds) - } - - /// The start `Index`. - public var startIndex: Int { - - return 0 - } - - /// The end `Index`. - /// - /// This is the "one-past-the-end" position, and will always be equal to the `count`. - public var endIndex: Int { - return count - } - - public func index(before i: Int) -> Int { - return i - 1 - } - - public func index(after i: Int) -> Int { - return i + 1 - } - - public func makeIterator() -> IndexingIterator { - - return IndexingIterator(_elements: self) - } -} - // MARK: - CoreGraphics public enum CGImageSourceOption: String { @@ -87,11 +39,9 @@ public func CGImageSourceCreateWithData(_ data: Data, _ options: [CGImageSourceO @inline(__always) public func CGImageSourceGetType(_ imageSource: T) -> String { - - return T.typeIdentifier + T.typeIdentifier } public func CGImageSourceCopyTypeIdentifiers() -> [String] { - - return [CGImageSourcePNG.typeIdentifier] + [CGImageSourcePNG.typeIdentifier] } diff --git a/Sources/Silica/CGImageSourcePNG.swift b/Sources/Silica/CGImageSourcePNG.swift index a583225..5ea269f 100644 --- a/Sources/Silica/CGImageSourcePNG.swift +++ b/Sources/Silica/CGImageSourcePNG.swift @@ -7,9 +7,9 @@ // #if os(macOS) - import Darwin.C.math -#elseif os(Linux) - import Glibc +import Darwin.C.math +#elseif canImport(Glibc) +import Glibc #endif import struct Foundation.Data @@ -19,7 +19,7 @@ public final class CGImageSourcePNG: CGImageSource { // MARK: - Class Properties - public static let typeIdentifier = "public.png" + public static var typeIdentifier: String { "public.png" } // MARK: - Properties @@ -38,9 +38,7 @@ public final class CGImageSourcePNG: CGImageSource { // MARK: - Methods public func createImage(at index: Int) -> CGImage? { - let image = CGImage(surface: surface) - return image } } diff --git a/Tests/SilicaTests/Utilities/Cacao/UIBezierPath.swift b/Sources/Silica/UIKit/UIBezierPath.swift similarity index 99% rename from Tests/SilicaTests/Utilities/Cacao/UIBezierPath.swift rename to Sources/Silica/UIKit/UIBezierPath.swift index fb5ebe6..10183ce 100644 --- a/Tests/SilicaTests/Utilities/Cacao/UIBezierPath.swift +++ b/Sources/Silica/UIKit/UIBezierPath.swift @@ -7,13 +7,12 @@ // #if os(macOS) - import Darwin.C.math +import Darwin.C.math #elseif os(Linux) - import Glibc +import Glibc #endif import Foundation -import Silica /// The `UIBezierPath` class lets you define a path consisting of straight and curved line segments /// and render that path in your custom views. You use this class initially to specify just the geometry for your path. diff --git a/Tests/SilicaTests/Utilities/Cacao/UIColor.swift b/Sources/Silica/UIKit/UIColor.swift similarity index 85% rename from Tests/SilicaTests/Utilities/Cacao/UIColor.swift rename to Sources/Silica/UIKit/UIColor.swift index ace0f52..e6d0613 100644 --- a/Tests/SilicaTests/Utilities/Cacao/UIColor.swift +++ b/Sources/Silica/UIKit/UIColor.swift @@ -7,7 +7,6 @@ // import Foundation -@testable import Silica public final class UIColor { @@ -52,32 +51,29 @@ public final class UIColor { /// Sets the color of subsequent stroke and fill operations to the color that the receiver represents. public func set() { - setFill() setStroke() } /// Sets the color of subsequent fill operations to the color that the receiver represents. public func setFill() { - UIGraphicsGetCurrentContext()?.fillColor = self.cgColor } /// Sets the color of subsequent stroke operations to the color that the receiver represents. public func setStroke() { - UIGraphicsGetCurrentContext()?.strokeColor = self.cgColor } // MARK: - Singletons - public static let red = UIColor(cgColor: .red) + public static var red: UIColor { UIColor(cgColor: .red) } - public static let green = UIColor(cgColor: .green) + public static var green: UIColor { UIColor(cgColor: .green) } - public static let blue = UIColor(cgColor: .blue) + public static var blue: UIColor { UIColor(cgColor: .blue) } - public static let white = UIColor(cgColor: .white) + public static var white: UIColor { UIColor(cgColor: .white) } - public static let black = UIColor(cgColor: .black) + public static var black: UIColor { UIColor(cgColor: .black) } } diff --git a/Tests/SilicaTests/Utilities/Cacao/UIGraphics.swift b/Sources/Silica/UIKit/UIGraphics.swift similarity index 81% rename from Tests/SilicaTests/Utilities/Cacao/UIGraphics.swift rename to Sources/Silica/UIKit/UIGraphics.swift index 4e86901..26f3b66 100644 --- a/Tests/SilicaTests/Utilities/Cacao/UIGraphics.swift +++ b/Sources/Silica/UIKit/UIGraphics.swift @@ -6,8 +6,6 @@ // Copyright © 2016 PureSwift. All rights reserved. // -import Silica - /// Returns the current graphics context. /// /// The current graphics context is `nil` by default. @@ -17,25 +15,21 @@ import Silica /// /// This function may be called from any thread of your app. public func UIGraphicsGetCurrentContext() -> CGContext? { - - return ContextStack.last + UIKitContextStack.last } /// Makes the specified graphics context the current context. public func UIGraphicsPushContext(_ context: CGContext) { - - ContextStack.append(context) + UIKitContextStack.append(context) } /// Removes the current graphics context from the top of the stack, restoring the previous context. public func UIGraphicsPopContext() { - - if ContextStack.isEmpty == false { - - ContextStack.removeLast() + if UIKitContextStack.isEmpty == false { + UIKitContextStack.removeLast() } } // MARK: - Private -private var ContextStack: [CGContext] = [] +nonisolated(unsafe) private var UIKitContextStack: [CGContext] = [] diff --git a/Tests/SilicaTests/Utilities/Cacao/UIImage.swift b/Sources/Silica/UIKit/UIImage.swift similarity index 90% rename from Tests/SilicaTests/Utilities/Cacao/UIImage.swift rename to Sources/Silica/UIKit/UIImage.swift index 0228764..012a3b3 100644 --- a/Tests/SilicaTests/Utilities/Cacao/UIImage.swift +++ b/Sources/Silica/UIKit/UIImage.swift @@ -7,19 +7,17 @@ // import Foundation -import Silica +/// UIKit compatibility layer for UIImage public final class UIImage { public let cgImage: Silica.CGImage public init(cgImage: Silica.CGImage) { - self.cgImage = cgImage } public var size: CGSize { - return CGSize(width: CGFloat(cgImage.width), height: CGFloat(cgImage.height)) } } diff --git a/Sources/Silica/UIKit/UIRectCorner.swift b/Sources/Silica/UIKit/UIRectCorner.swift new file mode 100644 index 0000000..d4001e2 --- /dev/null +++ b/Sources/Silica/UIKit/UIRectCorner.swift @@ -0,0 +1,28 @@ +// +// UIRectCorner.swift +// Cacao +// +// Created by Alsey Coleman Miller on 6/15/17. +// + +/// The corners of a rectangle. +/// +/// The specified constants reflect the corners of a rectangle that has not been modified by an affine transform and is drawn in +/// the default coordinate system (where the origin is in the upper-left corner and positive values extend down and to the right). +public struct UIRectCorner: OptionSet { + + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } +} + +public extension UIRectCorner { + + static var topLeft: UIRectCorner { UIRectCorner(rawValue: 1 << 0) } + static var topRight: UIRectCorner { UIRectCorner(rawValue: 1 << 1) } + static var bottomLeft: UIRectCorner { UIRectCorner(rawValue: 1 << 2) } + static var bottomRight: UIRectCorner { UIRectCorner(rawValue: 1 << 3) } + static var allCorners: UIRectCorner { UIRectCorner(rawValue: ~0) } +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index bb7d22c..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// main.swift -// Silica -// -// Created by Alsey Coleman Miller on 6/1/16. -// Copyright © 2016 PureSwift. All rights reserved. -// - -import XCTest -@testable import Silica -@testable import SilicaTests - -XCTMain([ - testCase(FontTests.allTests), - testCase(StyleKitTests.allTests) - ]) diff --git a/Tests/SilicaTests/FontTests.swift b/Tests/SilicaTests/FontTests.swift index 1237555..d0381bf 100644 --- a/Tests/SilicaTests/FontTests.swift +++ b/Tests/SilicaTests/FontTests.swift @@ -10,9 +10,7 @@ import XCTest @testable import Silica final class FontTests: XCTestCase { - - static let allTests = [("testCreateFont", testCreateFont)] - + func testCreateFont() { #if os(Linux) @@ -36,7 +34,7 @@ final class FontTests: XCTestCase { for (fontName, expectedFullName) in fontNames { guard let font = Silica.CGFont(name: fontName) - else { XCTFail("Could not create font"); return } + else { XCTFail("Could not create font \(fontName)"); return } XCTAssert(font.name == font.name) XCTAssert(expectedFullName == font.scaledFont.fullName, "\(expectedFullName) == \(font.scaledFont.fullName)") diff --git a/Tests/SilicaTests/StyleKitTests.swift b/Tests/SilicaTests/StyleKitTests.swift index 2174389..ff494b1 100644 --- a/Tests/SilicaTests/StyleKitTests.swift +++ b/Tests/SilicaTests/StyleKitTests.swift @@ -13,10 +13,6 @@ import Cairo final class StyleKitTests: XCTestCase { - static let allTests = [("testSimpleShapes", testSimpleShapes), - ("testAdvancedShapes", testAdvancedShapes), - ("testImagePNG", testImagePNG)] - private func draw(_ drawingMethod: () -> (), _ name: String, _ size: CGSize) { let filename = TestPath.testData + name + ".pdf" @@ -46,14 +42,10 @@ final class StyleKitTests: XCTestCase { draw(TestStyleKit.drawAdvancedShapes, "advancedShapes", CGSize(width: 240, height: 120)) } - func testImagePNG() { - - do { try TestAssetManager.shared.fetchAssets() } + func testImagePNG() async throws { - catch { XCTFail("Could not get test assets (\(error))"); return } + try await TestAssetManager.shared.fetchAssets() draw(TestStyleKit.drawImagePNG, "imagePNG", CGSize(width: 240, height: 180)) } } - - diff --git a/Tests/SilicaTests/Utilities/Cacao/UIRectCorner.swift b/Tests/SilicaTests/Utilities/Cacao/UIRectCorner.swift deleted file mode 100644 index 97da097..0000000 --- a/Tests/SilicaTests/Utilities/Cacao/UIRectCorner.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// UIRectCorner.swift -// Cacao -// -// Created by Alsey Coleman Miller on 6/15/17. -// - -/// -public struct UIRectCorner: OptionSet { - - public let rawValue: Int - - public init(rawValue: Int) { - self.rawValue = rawValue - } - - public static let topLeft = UIRectCorner(rawValue: 1 << 0) - public static let topRight = UIRectCorner(rawValue: 1 << 1) - public static let bottomLeft = UIRectCorner(rawValue: 1 << 2) - public static let bottomRight = UIRectCorner(rawValue: 1 << 3) - public static let allCorners = UIRectCorner(rawValue: ~0) -} diff --git a/Tests/SilicaTests/Utilities/Define.swift b/Tests/SilicaTests/Utilities/Define.swift index 3a853e0..ef365c4 100644 --- a/Tests/SilicaTests/Utilities/Define.swift +++ b/Tests/SilicaTests/Utilities/Define.swift @@ -7,6 +7,9 @@ // import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif struct TestPath { @@ -37,16 +40,13 @@ struct TestPath { } } -extension TestAssetManager { +extension TestAssetManager where HTTPClient == URLSession { - static var shared: TestAssetManager { - - struct Static { - static let value = TestAssetManager(assets: testAssets, cacheDirectory: URL(fileURLWithPath: TestPath.assets)) - } - - return Static.value - } + nonisolated(unsafe) static let shared: TestAssetManager = TestAssetManager( + assets: testAssets, + cacheDirectory: URL(fileURLWithPath: TestPath.assets), + httpClient: URLSession(configuration: .ephemeral) + ) } private let testAssets: [TestAsset] = [ diff --git a/Tests/SilicaTests/Utilities/HTTP/HTTP.swift b/Tests/SilicaTests/Utilities/HTTP/HTTP.swift deleted file mode 100755 index 8465f9a..0000000 --- a/Tests/SilicaTests/Utilities/HTTP/HTTP.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// HTTP.swift -// SwiftFoundation -// -// Created by Alsey Coleman Miller on 8/9/15. -// Copyright © 2015 PureSwift. All rights reserved. -// - -import struct Foundation.URL - -public struct HTTP: URLProtocol { - - public static func validURL(URL: URL) -> Bool { - - guard (URL.scheme == "http" || URL.scheme == "https") else { return false } - - return true - } -} diff --git a/Tests/SilicaTests/Utilities/HTTP/HTTPClient.swift b/Tests/SilicaTests/Utilities/HTTP/HTTPClient.swift deleted file mode 100644 index cdd9227..0000000 --- a/Tests/SilicaTests/Utilities/HTTP/HTTPClient.swift +++ /dev/null @@ -1,139 +0,0 @@ -// -// HTTPClient.swift -// SwiftFoundation -// -// Created by Alsey Coleman Miller on 9/02/15. -// Copyright © 2015 PureSwift. All rights reserved. -// - -import Foundation - -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif - -import Dispatch - -#if canImport(FoundationNetworking) -typealias FoundationURLRequest = FoundationNetworking.URLRequest -typealias FoundationURLResponse = FoundationNetworking.URLResponse -#else -typealias FoundationURLRequest = Foundation.URLRequest -typealias FoundationURLResponse = Foundation.URLResponse -#endif - -// Dot notation syntax for class -public extension HTTP { - - /// Loads HTTP requests - public final class Client { - - public init(session: URLSession? = nil) { - - if let session = session { - - self.session = session - - } else { - - #if os(macOS) - self.session = URLSession.shared - #else - self.session = URLSession(configuration: URLSessionConfiguration()) - #endif - } - } - - /// The backing ```NSURLSession```. - public let session: URLSession - - public func send(request: HTTP.Request) throws -> HTTP.Response { - - var dataTask: URLSessionDataTask? - - return try send(request: request, dataTask: &dataTask) - } - - public func send(request: HTTP.Request, dataTask: inout URLSessionDataTask?) throws -> HTTP.Response { - - // build request... - - guard let urlRequest = FoundationURLRequest(request: request) - else { throw Error.BadRequest } - - // execute request - - let semaphore = DispatchSemaphore(value: 0); - - var error: Swift.Error? - - var responseData: Data? - - var urlResponse: HTTPURLResponse? - - dataTask = self.session.dataTask(with: urlRequest) { (data: Foundation.Data?, response: FoundationURLResponse?, responseError: Swift.Error?) -> () in - - responseData = data - - urlResponse = response as? HTTPURLResponse - - error = responseError - - semaphore.signal() - } - - dataTask!.resume() - - // wait for task to finish - - let _ = semaphore.wait(timeout: DispatchTime.distantFuture); - - guard urlResponse != nil else { throw error! } - - var response = HTTP.Response() - - response.statusCode = urlResponse!.statusCode - - if let data = responseData, data.count > 0 { - - response.body = data - } - - response.headers = urlResponse!.allHeaderFields as! [String: String] - - response.url = urlResponse!.url - - return response - } - } -} - - -public extension HTTP.Client { - - public enum Error: Swift.Error { - - /// The provided request was malformed. - case BadRequest - } -} - -public extension FoundationURLRequest { - - init?(request: HTTP.Request) { - - guard request.version == HTTP.Version(1, 1) else { return nil } - - self.init(url: request.url, timeoutInterval: request.timeoutInterval) - - if request.body.isEmpty == false { - - self.httpBody = request.body - } - - self.allHTTPHeaderFields = request.headers - - self.httpMethod = request.method.rawValue - } -} - diff --git a/Tests/SilicaTests/Utilities/HTTP/HTTPMethod.swift b/Tests/SilicaTests/Utilities/HTTP/HTTPMethod.swift deleted file mode 100755 index 5b47f53..0000000 --- a/Tests/SilicaTests/Utilities/HTTP/HTTPMethod.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// HTTPMethod.swift -// SwiftFoundation -// -// Created by Alsey Coleman Miller on 6/29/15. -// Copyright © 2015 PureSwift. All rights reserved. -// - -public extension HTTP { - - /// HTTP Method. - public enum Method: String { - - case GET - case PUT - case DELETE - case POST - case OPTIONS - case HEAD - case TRACE - case CONNECT - case PATCH - - init() { self = .GET } - } -} - diff --git a/Tests/SilicaTests/Utilities/HTTP/HTTPRequest.swift b/Tests/SilicaTests/Utilities/HTTP/HTTPRequest.swift deleted file mode 100755 index 9b97ee8..0000000 --- a/Tests/SilicaTests/Utilities/HTTP/HTTPRequest.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// HTTPRequest.swift -// SwiftFoundation -// -// Created by Alsey Coleman Miller on 6/29/15. -// Copyright © 2015 PureSwift. All rights reserved. -// - -import Foundation - -public extension HTTP { - - /// HTTP request. - public struct Request: URLRequest { - - public var url: URL - - public var timeoutInterval: TimeInterval - - public var body: Data - - public var headers: [String: String] - - public var method: HTTP.Method - - public var version: HTTP.Version - - public init(url: URL, - timeoutInterval: TimeInterval = 30, - body: Data = Data(), - headers: [String: String] = [:], - method: HTTP.Method = .GET, - version: HTTP.Version = HTTP.Version()) { - - self.url = url - self.timeoutInterval = timeoutInterval - self.body = body - self.headers = headers - self.method = method - self.version = version - } - } -} - diff --git a/Tests/SilicaTests/Utilities/HTTP/HTTPResponse.swift b/Tests/SilicaTests/Utilities/HTTP/HTTPResponse.swift deleted file mode 100755 index bd9de02..0000000 --- a/Tests/SilicaTests/Utilities/HTTP/HTTPResponse.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// HTTPResponse.swift -// SwiftFoundation -// -// Created by Alsey Coleman Miller on 6/29/15. -// Copyright © 2015 PureSwift. All rights reserved. -// - -import struct Foundation.URL -import struct Foundation.Data - -public extension HTTP { - - /// HTTP URL response. - public struct Response: URLResponse { - - /// Returns a dictionary containing all the HTTP header fields. - public var headers: [String: String] - - /// Returns the HTTP status code for the response. - public var statusCode: Int - - /// The HTTP response body. - public var body: Data - - /// The URL for the response. - /// - /// Returned with 302 Found response. - public var url: URL? - - public init(headers: [String: String] = [String: String](), - statusCode: Int = HTTP.StatusCode.OK.rawValue, - body: Data = Data(), - url: URL? = nil) { - - self.headers = headers - self.statusCode = statusCode - self.body = body - self.url = url - } - } -} - diff --git a/Tests/SilicaTests/Utilities/HTTP/HTTPStatusCode.swift b/Tests/SilicaTests/Utilities/HTTP/HTTPStatusCode.swift deleted file mode 100755 index d3293c9..0000000 --- a/Tests/SilicaTests/Utilities/HTTP/HTTPStatusCode.swift +++ /dev/null @@ -1,517 +0,0 @@ -// -// HTTPStatusCode.swift -// SwiftFoundation -// -// Created by Alsey Coleman Miller on 6/29/15. -// Copyright © 2015 PureSwift. All rights reserved. -// - -public extension HTTP { - - /// The standard status codes used with the [HTTP](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) protocol. - public enum StatusCode: Int { - - /// Initializes to 200. - public init() { self = .OK } - - // MARK: - 1xx Informational - - /// Continue - /// - /// This means that the server has received the request headers, - /// and that the client should proceed to send the request body - /// (in the case of a request for which a body needs to be sent; for example, a POST request). - /// If the request body is large, sending it to a server when a request has already been rejected - /// based upon inappropriate headers is inefficient. - /// To have a server check if the request could be accepted based on the request's headers alone, - /// a client must send Expect: 100-continue as a header in its initial request and check if a 100 - /// Continue status code is received in response before continuing - /// (or receive 417 Expectation Failed and not continue). - case Continue = 100 - - /// Switching Protocols - /// - /// This means the requester has asked the server to switch protocols and the server - /// is acknowledging that it will do so. - case SwitchingProtocols = 101 - - /// Processing (WebDAV; RFC 2518) - /// - /// As a WebDAV request may contain many sub-requests involving file operations, - /// it may take a long time to complete the request. - /// This code indicates that the server has received and is processing the request, - /// but no response is available yet. - /// This prevents the client from timing out and assuming the request was lost. - case Processing = 102 - - // MARK: - 2xx Success - - /// OK - /// - /// Standard response for successful HTTP requests. - /// The actual response will depend on the request method used. - /// In a GET request, the response will contain an entity corresponding to the requested resource. - /// In a POST request, the response will contain an entity describing or containing - /// the result of the action. - case OK = 200 - - /// Created - /// - /// The request has been fulfilled and resulted in a new resource being created. - case Created = 201 - - /// Accepted - /// - /// The request has been accepted for processing, but the processing has not been completed. - /// The request might or might not eventually be acted upon, - /// as it might be disallowed when processing actually takes place. - case Accepted = 202 - - /// Non-Authoritative Information (since HTTP/1.1) - /// - /// The server successfully processed the request, - /// but is returning information that may be from another source. - case NonAuthoritativeInformation = 203 - - /// No Content - /// - /// The server successfully processed the request, but is not returning any content. - case NoContent = 204 - - /// Reset Content - /// - /// The server successfully processed the request, but is not returning any content. - /// Unlike a 204 response, this response requires that the requester reset the document view. - case ResetContent = 205 - - /// Partial Content ([RFC 7233](https://tools.ietf.org/html/rfc7233)) - /// - /// The server is delivering only part of the resource - /// ([byte serving](https://en.wikipedia.org/wiki/Byte_serving)) due to a range header sent - /// by the client. The range header is used by HTTP clients to enable resuming of interrupted - /// downloads, or split a download into multiple simultaneous streams. - case PartialContent = 206 - - /// Multi-Status (WebDAV; RFC 4918) - /// - /// The message body that follows is an XML message and can contain a number of separate response - /// codes, depending on how many sub-requests were made. - case MultiStatus = 207 - - /// Already Reported (WebDAV; RFC 5842) - /// - /// The members of a DAV binding have already been enumerated in a previous reply to this request, - /// and are not being included again. - case AlreadyReported = 208 - - /// IM Used (RFC 3229) - /// - /// The server has fulfilled a request for the resource, and the response is a representation of the - /// result of one or more instance-manipulations applied to the current instance. - case IMUsed = 226 - - // MARK: - 3xx Redirection - - /// Multiple Choices - /// - /// Indicates multiple options for the resource that the client may follow. - /// It, for instance, could be used to present different format options for video, - /// list files with different extensions, or word sense disambiguation. - case MultipleChoices = 300 - - /// [Moved Permanently](https://en.wikipedia.org/wiki/HTTP_301) - /// - /// This and all future requests should be directed to the given URI. - case MovedPermanently = 301 - - /// [Found](https://en.wikipedia.org/wiki/HTTP_302) - /// - /// This is an example of industry practice contradicting the standard. - /// The HTTP/1.0 specification (RFC 1945) required the client to perform a temporary redirect - /// (the original describing phrase was "Moved Temporarily"), but popular browsers implemented 302 - /// with the functionality of a 303. - /// - /// Therefore, HTTP/1.1 added status codes 303 and 307 to distinguish between the two behaviours. - /// However, some Web applications and frameworks use the 302 status code as if it were the 303. - case Found = 302 - - /// See Other (since HTTP/1.1) - /// - /// The response to the request can be found under another URI using a GET method. - /// When received in response to a POST (or PUT/DELETE), it should be assumed that the server - /// has received the data and the redirect should be issued with a separate GET message. - case SeeOther = 303 - - /// Not Modified ([RFC 7232](https://tools.ietf.org/html/rfc7232)) - /// - /// Indicates that the resource has not been modified since the version specified by the request - /// headers If-Modified-Since or If-None-Match. - /// This means that there is no need to retransmit the resource, - /// since the client still has a previously-downloaded copy. - case NotModified = 304 - - /// Use Proxy (since HTTP/1.1) - /// - /// The requested resource is only available through a proxy, whose address is provided in the - /// response. Many HTTP clients (such as Mozilla and Internet Explorer) do not correctly handle - /// responses with this status code, primarily for security reasons - case UseProxy = 305 - - /// Switch Proxy - /// - /// No longer used. Originally meant "Subsequent requests should use the specified proxy." - case SwitchProxy = 306 - - /// Temporary Redirect (since HTTP/1.1) - /// - /// In this case, the request should be repeated with another URI; - /// however, future requests should still use the original URI. - /// - /// In contrast to how 302 was historically implemented, the request method is not allowed to be - /// changed when reissuing the original request. - /// For instance, a POST request should be repeated using another POST request. - case TemporaryRedirect = 307 - - /// Permanent Redirect (RFC 7538) - /// - /// The request, and all future requests should be repeated using another URI. 307 and 308 - /// (as proposed) parallel the behaviours of 302 and 301, but do not allow the HTTP method to change. - /// So, for example, submitting a form to a permanently redirected resource may continue smoothly. - case PermanentRedirect = 308 - - // MARK: - 4xx Client Error - - /// Bad Request - /// - /// The server cannot or will not process the request due to something that is perceived to be a client - /// error (e.g., malformed request syntax, invalid request message framing, - /// or deceptive request routing) - case BadRequest = 400 - - /// Unauthorized ([RFC 7235](https://tools.ietf.org/html/rfc7235)) - /// - /// Similar to **403 Forbidden**, but specifically for use when authentication is required and has - /// failed or has not yet been provided. - /// The response must include a WWW-Authenticate header field containing - /// a challenge applicable to the requested resource. - case Unauthorized = 401 - - /// Payment Required - /// - /// Reserved for future use. - /// The original intention was that this code might be used as part of some form of digital cash or - /// micropayment scheme, but that has not happened, and this code is not usually used. - /// - /// [YouTube](youtube.com) uses this status if a particular IP address has made excessive requests, - /// and requires the person to enter a CAPTCHA. - case PaymentRequired = 402 - - /// [Forbidden](https://en.wikipedia.org/wiki/HTTP_403) - /// - /// The request was a valid request, but the server is refusing to respond to it. - /// Unlike a 401 Unauthorized response, authenticating will make no difference. - case Forbidden = 403 - - /// [Not Found](https://en.wikipedia.org/wiki/HTTP_404) - /// - /// The requested resource could not be found but may be available again in the future. Subsequent requests by the client are permissible. - case NotFound = 404 - - /// Method Not Allowed - /// - /// A request was made of a resource using a request method not supported by that resource; - /// for example, using GET on a form which requires data to be presented via POST, - /// or using PUT on a read-only resource. - case MethodNotAllowed = 405 - - /// Not Acceptable - /// - /// The requested resource is only capable of generating content not acceptable according to the - /// **Accept** headers sent in the request. - case NotAcceptable = 406 - - /// Proxy Authentication Required ([RFC 7235](https://tools.ietf.org/html/rfc7235)) - /// - /// The client must first authenticate itself with the proxy. - case ProxyAuthenticationRequired = 407 - - /// Request Timeout - /// - /// The server timed out waiting for the request. According to HTTP specifications: - /// - /// "The client did not produce a request within the time that the server was prepared to wait. - /// The client MAY repeat the request without modifications at any later time." - case RequestTimeout = 408 - - /// Conflict - /// - /// Indicates that the request could not be processed because of conflict in the request, - /// such as an [edit conflict](https://en.wikipedia.org/wiki/Edit_conflict) - /// in the case of multiple updates. - case Conflict = 409 - - /// Gone - /// - /// Indicates that the resource requested is no longer available and will not be available again. - /// This should be used when a resource has been intentionally removed and the resource should be - /// purged. Upon receiving a 410 status code, the client should not request the resource again in the - /// future. Clients such as search engines should remove the resource from their indices. - /// Most use cases do not require clients and search engines to purge the resource, - /// and a **404 Not Found** may be used instead. - case Gone = 410 - - /// Length Required - /// - /// The request did not specify the length of its content, which is required by the requested resource. - case LengthRequired = 411 - - /// Precondition Failed ([RFC 7232](https://tools.ietf.org/html/rfc7232)) - /// - /// The server does not meet one of the preconditions that the requester put on the request. - case PreconditionFailed = 412 - - /// Payload Too Large ([RFC 7231](https://tools.ietf.org/html/rfc7231)) - /// - /// The request is larger than the server is willing or able to process. - /// - /// Called "Request Entity Too Large " previously. - case PayloadTooLarge = 413 - - /// Request-URI Too Long - /// - /// The URI provided was too long for the server to process. - /// Often the result of too much data being encoded as a query-string of a GET request, - /// in which case it should be converted to a POST request. - case RequestURITooLong = 414 - - /// Unsupported Media Type - /// - /// The request entity has a [media type](https://en.wikipedia.org/wiki/Internet_media_type) - /// which the server or resource does not support. - /// - /// For example, the client uploads an image as image/svg+xml, - /// but the server requires that images use a different format. - case UnsupportedMediaType = 415 - - /// Requested Range Not Satisfiable ([RFC 7233](https://tools.ietf.org/html/rfc7233)) - /// - /// The client has asked for a portion of the file - /// ([byte serving](https://en.wikipedia.org/wiki/Byte_serving)), - /// but the server cannot supply that portion. - /// - /// For example, if the client asked for a part of the file that lies beyond the end of the file. - case RequestedRangeNotSatisfiable = 416 - - /// Expectation Failed - /// - /// The server cannot meet the requirements of the **Expect** request-header field. - case ExpectationFailed = 417 - - /// I'm a teapot (RFC 2324) - /// - /// This code was defined in 1998 as one of the traditional IETF April Fools' jokes, - /// in [RFC 2324](https://tools.ietf.org/html/rfc2324), - /// [Hyper Text Coffee Pot Control Protocol](https://en.wikipedia.org/wiki/Hyper_Text_Coffee_Pot_Control_Protocol), - /// and is not expected to be implemented by actual HTTP servers. - /// - /// The RFC specifies this code should be returned by tea pots requested to brew coffee. - case Teapot = 418 - - /// Authentication Timeout (not in [RFC 2616](https://tools.ietf.org/html/rfc2616)) - /// - /// Not a part of the HTTP standard, **419 Authentication Timeout** denotes that previously valid - /// authentication has expired. It is used as an alternative to **401 Unauthorized** - /// in order to differentiate from otherwise authenticated clients being denied access to - /// specific server resources. - case AuthenticationTimeout = 419 - - /// Enhance Your Calm ([Twitter](https://en.wikipedia.org/wiki/Twitter)) - /// - /// Not part of the HTTP standard, but returned by version 1 of the Twitter Search and - /// Trends API when the client is being rate limited. - /// Other services may wish to implement the - /// [429 Too Many Requests](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#429) - /// response code instead. - case EnhanceYourCalm = 420 - - /// Misdirected Request ([HTTP/2](https://en.wikipedia.org/wiki/HTTP/2)) - /// - /// The request was directed at a server that is not able to produce a response - /// (for example because a connection reuse). - case MisdirectedRequest = 421 - - /// Unprocessable Entity (WebDAV; RFC 4918) - /// - /// The request was well-formed but was unable to be followed due to semantic errors. - case UnprocessableEntity = 422 - - /// Locked (WebDAV; RFC 4918) - /// - /// The resource that is being accessed is locked. - case Locked = 423 - - /// Failed Dependency (WebDAV; RFC 4918) - /// - /// The request failed due to failure of a previous request (e.g., a PROPPATCH). - case FailedDependency = 424 - - /// Upgrade Required - /// - /// The client should switch to a different protocol such as - /// [TLS/1.0](https://en.wikipedia.org/wiki/Transport_Layer_Security), - /// given in the [Upgrade](https://en.wikipedia.org/wiki/Upgrade_header) header field. - case UpgradeRequired = 426 - - /// Precondition Required ([RFC 6585](https://tools.ietf.org/html/rfc6585)) - /// - /// The origin server requires the request to be conditional. - /// Intended to prevent "the 'lost update' problem, where a client GETs a resource's state, - /// modifies it, and PUTs it back to the server, - /// when meanwhile a third party has modified the state on the server, leading to a conflict." - case PreconditionRequired = 428 - - /// Too Many Requests ([RFC 6585](https://tools.ietf.org/html/rfc6585)) - /// - /// The user has sent too many requests in a given amount of time. - /// Intended for use with rate limiting schemes. - case TooManyRequests - - /// Request Header Fields Too Large ([RFC 6585](https://tools.ietf.org/html/rfc6585)) - /// - /// The server is unwilling to process the request because either an individual header field, - /// or all the header fields collectively, are too large. - case RequestHeaderFieldsTooLarge = 431 - - /// Login Timeout (Microsoft) - /// - /// A Microsoft extension. Indicates that your session has expired. - case LoginTimeout = 440 - - /// No Response (Nginx) - /// - /// Used in Nginx logs to indicate that the server has returned no information to the client - /// and closed the connection (useful as a deterrent for malware). - case NoResponse = 444 - - /// Retry With (Microsoft) - /// - /// A Microsoft extension. The request should be retried after performing the appropriate action. - case RetryWith = 449 - - /// Blocked by Windows Parental Controls (Microsoft) - /// - /// A Microsoft extension. - /// This error is given when Windows Parental Controls are turned on and are - /// blocking access to the given webpage. - case BlockedByParentalControls = 450 - - /// [Unavailable For Legal Reasons](https://en.wikipedia.org/wiki/HTTP_451) (Internet draft) - /// - /// Defined in the internet draft "A New HTTP Status Code for Legally-restricted Resources". - /// Intended to be used when resource access is denied for legal reasons, - /// e.g. censorship or government-mandated blocked access. - /// A reference to the 1953 dystopian novel Fahrenheit 451, where books are outlawed. - case UnavailableForLegalReasons = 451 - - /// Request Header Too Large (Nginx) - /// - /// Nginx internal code similar to 431 but it was introduced earlier in version 0.9.4 - /// (on January 21, 2011). - case RequestHeaderTooLarge = 494 - - /// Cert Error (Nginx) - /// - /// Nginx internal code used when SSL client certificate error occurred to distinguish it - /// from 4XX in a log and an error page redirection. - case CertError = 495 - - // MARK: - 5xx Server Error - - /// Internal Server Error - /// - /// A generic error message, given when an unexpected condition was encountered and - /// no more specific message is suitable. - case InternalServerError = 500 - - /// Not Implemented - /// - /// The server either does not recognize the request method, - /// or it lacks the ability to fulfill the request. - /// Usually this implies future availability (e.g., a new feature of a web-service API). - case NotImplemented = 501 - - /// Bad Gateway - /// - /// The server was acting as a gateway or proxy and received an invalid response - /// from the upstream server. - case BadGateway = 502 - - /// Service Unavailable - /// - /// The server is currently unavailable (because it is overloaded or down for maintenance). - /// Generally, this is a temporary state. - case ServiceUnavailable = 503 - - /// Gateway Timeout - /// - /// The server was acting as a gateway or proxy and did not receive a timely response - /// from the upstream server. - case GatewayTimeout = 504 - - /// Version Not Supported - /// - /// The server does not support the HTTP protocol version used in the request. - case VersionNotSupported = 505 - - /// Variant Also Negotiates (RFC 2295) - /// - /// Transparent content negotiation for the request results in a circular reference. - case VariantAlsoNegotiates = 506 - - /// Insufficient Storage (WebDAV; RFC 4918) - /// - /// The server is unable to store the representation needed to complete the request. - case InsufficientStorage = 507 - - /// Loop Detected (WebDAV; RFC 5842) - /// - /// The server detected an infinite loop while processing the request (sent in lieu of - /// [208 Already Reported](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#208)). - case LoopDetected = 508 - - /// Bandwidth Limit Exceeded (Apache bw/limited extension) - /// - /// This status code is not specified in any RFCs. Its use is unknown. - case BandwidthLimitExceeded = 509 - - /// Not Extended (RFC 2774) - /// - /// Further extensions to the request are required for the server to fulfil it. - case NotExtended = 510 - - /// Network Authentication Required ([RFC 6585](https://tools.ietf.org/html/rfc6585)) - /// - /// The client needs to authenticate to gain network access. - /// Intended for use by intercepting proxies used to control access to the network - /// (e.g., "captive portals" used to require agreement to Terms of Service before granting - /// full Internet access via a Wi-Fi hotspot). - case NetworkAuthenticationRequired = 511 - - /// Unknown Error - /// - /// This status code is not specified in any RFC and is returned by certain services, - /// for instance Microsoft Azure and CloudFlare servers: - /// - /// "The 520 error is essentially a “catch-all” response for when the origin server returns something - /// unexpected or something that is not tolerated/interpreted (protocol violation or empty response)." - case UnknownError = 520 - - /// Origin Connection Time-out - /// - /// This status code is not specified in any RFCs, - /// but is used by CloudFlare's reverse proxies to signal that a server connection timed out. - case OriginConnectionTimeOut = 522 - - } -} - diff --git a/Tests/SilicaTests/Utilities/HTTP/HTTPVersion.swift b/Tests/SilicaTests/Utilities/HTTP/HTTPVersion.swift deleted file mode 100755 index d9c9b56..0000000 --- a/Tests/SilicaTests/Utilities/HTTP/HTTPVersion.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// HTTPVersion.swift -// SwiftFoundation -// -// Created by Alsey Coleman Miller on 6/29/15. -// Copyright © 2015 PureSwift. All rights reserved. -// - -public extension HTTP { - - /// Defines the HTTP protocol version. - public struct Version { - - public typealias ValueType = UInt8 - - /// Major version number. - public var major: ValueType - - /// Minor version number. - public var minor: ValueType - - /// Defualts to HTTP 1.1 - public init(_ major: ValueType = 1, _ minor: ValueType = 1) { - - self.major = major - self.minor = minor - } - } -} - -public func == (lhs: HTTP.Version, rhs: HTTP.Version) -> Bool { - - return lhs.major == rhs.major && lhs.minor == rhs.minor -} - diff --git a/Tests/SilicaTests/Utilities/HTTP/URLClient.swift b/Tests/SilicaTests/Utilities/HTTP/URLClient.swift deleted file mode 100755 index 2beff9b..0000000 --- a/Tests/SilicaTests/Utilities/HTTP/URLClient.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// URLClient.swift -// SwiftFoundation -// -// Created by Alsey Coleman Miller on 7/20/15. -// Copyright © 2015 PureSwift. All rights reserved. -// - -public protocol URLClient { - - associatedtype Request: URLRequest - - associatedtype Response: URLResponse - - func send(request: Request) throws -> Response -} \ No newline at end of file diff --git a/Tests/SilicaTests/Utilities/HTTP/URLProtocol.swift b/Tests/SilicaTests/Utilities/HTTP/URLProtocol.swift deleted file mode 100755 index 33c3ab9..0000000 --- a/Tests/SilicaTests/Utilities/HTTP/URLProtocol.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// URLProtocol.swift -// SwiftFoundation -// -// Created by Alsey Coleman Miller on 8/9/15. -// Copyright © 2015 PureSwift. All rights reserved. -// - -import Foundation - -public protocol URLProtocol { - - /// Checks whether the URL is valid for the protocol - static func validURL(URL: URL) -> Bool -} diff --git a/Tests/SilicaTests/Utilities/HTTP/URLRequest.swift b/Tests/SilicaTests/Utilities/HTTP/URLRequest.swift deleted file mode 100755 index efc0e37..0000000 --- a/Tests/SilicaTests/Utilities/HTTP/URLRequest.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// URLRequest.swift -// SwiftFoundation -// -// Created by Alsey Coleman Miller on 6/29/15. -// Copyright © 2015 PureSwift. All rights reserved. -// - -import struct Foundation.URL -import typealias Foundation.TimeInterval - -public protocol URLRequest { - - var url: URL { get } - - var timeoutInterval: TimeInterval { get } -} diff --git a/Tests/SilicaTests/Utilities/HTTP/URLResponse.swift b/Tests/SilicaTests/Utilities/HTTP/URLResponse.swift deleted file mode 100755 index 7ab22eb..0000000 --- a/Tests/SilicaTests/Utilities/HTTP/URLResponse.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// URLResponse.swift -// SwiftFoundation -// -// Created by Alsey Coleman Miller on 6/29/15. -// Copyright © 2015 PureSwift. All rights reserved. -// - -/// Encapsulates the metadata associated with the response to a a URL load request -/// in a manner independent of protocol and URL scheme. -public protocol URLResponse { - - -} diff --git a/Tests/SilicaTests/Utilities/TestAssets.swift b/Tests/SilicaTests/Utilities/TestAssets.swift index eed8958..2538d2f 100644 --- a/Tests/SilicaTests/Utilities/TestAssets.swift +++ b/Tests/SilicaTests/Utilities/TestAssets.swift @@ -8,47 +8,36 @@ import Foundation import Silica +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif -struct TestAsset { +struct TestAsset: Equatable, Hashable { let url: URL let filename: String } -extension TestAsset: Equatable { - - static func ==(lhs: TestAsset, rhs: TestAsset) -> Bool { - - return lhs.url == rhs.url - && lhs.filename == rhs.filename - } -} - -extension TestAsset: Hashable { - - var hashValue: Int { - - return (url.absoluteString + filename).hashValue - } -} - -final class TestAssetManager { - - init(assets: [TestAsset], cacheDirectory: URL) { - - self.assets = assets - self.cacheDirectory = cacheDirectory - } +final class TestAssetManager { let assets: [TestAsset] let cacheDirectory: URL - let httpClient = HTTP.Client() + let httpClient: HTTPClient private(set) var downloadedAssets = [TestAsset]() - func fetchAssets(skipCached: Bool = true) throws { + init(assets: [TestAsset], cacheDirectory: URL, httpClient: HTTPClient) { + + self.assets = assets + self.cacheDirectory = cacheDirectory + self.httpClient = httpClient + } + + public func fetchAssets(skipCached: Bool = true) async throws { + + let httpClient = self.httpClient for asset in assets { @@ -66,15 +55,17 @@ final class TestAssetManager { // fetch data - let request = HTTP.Request(url: asset.url) + let request = URLRequest(url: asset.url) - let response = try httpClient.send(request: request) + let (data, response) = try await httpClient.data(for: request) - guard response.statusCode == HTTP.StatusCode.OK.rawValue - else { throw Error.invalidStatusCode(response.statusCode) } - - let data = response.body + guard let httpResponse = response as? HTTPURLResponse else { + fatalError() + } + guard httpResponse.statusCode == 200 + else { throw Error.invalidStatusCode(httpResponse.statusCode) } + guard data.isEmpty == false else { throw Error.emptyData } @@ -90,9 +81,17 @@ final class TestAssetManager { } func cacheURL(for assetFilename: String) -> URL { - return cacheDirectory.appendingPathComponent(assetFilename) } + + func cachedImage(named name: String) -> CGImage? { + let fileURL = cacheURL(for: name) + guard let data = try? Data(contentsOf: fileURL), + let imageSource = CGImageSourcePNG(data: data), + let image = imageSource.createImage(at: 0) + else { return nil } + return image + } } extension TestAssetManager { @@ -110,15 +109,9 @@ extension TestAssetManager { extension UIImage { convenience init?(named name: String) { - - let fileURL = TestAssetManager.shared.cacheURL(for: name) - - guard let data = try? Data(contentsOf: fileURL), - let imageSource = CGImageSourcePNG(data: data) - else { return nil } - - let image = imageSource[0] - + guard let image = TestAssetManager.shared.cachedImage(named: name) else { + return nil + } self.init(cgImage: image) } } diff --git a/Tests/SilicaTests/Utilities/TestStyleKit.swift b/Tests/SilicaTests/Utilities/TestStyleKit.swift index 21a0d44..a7e975e 100644 --- a/Tests/SilicaTests/Utilities/TestStyleKit.swift +++ b/Tests/SilicaTests/Utilities/TestStyleKit.swift @@ -8,8 +8,8 @@ // Generated by PaintCode // http://www.paintcodeapp.com -#if os(Linux) - import Glibc +#if canImport(Glibc) +import Glibc #endif import Foundation @@ -20,7 +20,7 @@ public final class TestStyleKit : NSObject { //// Cache private struct Cache { - static let wirelessBlue: UIColor = UIColor(red: 0.278, green: 0.506, blue: 0.976, alpha: 1.000) + static var wirelessBlue: UIColor { UIColor(red: 0.278, green: 0.506, blue: 0.976, alpha: 1.000) } } //// Colors diff --git a/Tests/SilicaTests/Utilities/URLClient.swift b/Tests/SilicaTests/Utilities/URLClient.swift new file mode 100644 index 0000000..1ab6a7c --- /dev/null +++ b/Tests/SilicaTests/Utilities/URLClient.swift @@ -0,0 +1,48 @@ +// +// URLClient.swift +// +// +// Created by Alsey Coleman Miller on 8/21/23. +// + +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +/// URL Client +public protocol URLClient { + + func data(for request: URLRequest) async throws -> (Data, URLResponse) +} + +extension URLSession: URLClient { + + public func data(for request: URLRequest) async throws -> (Data, URLResponse) { + #if canImport(Darwin) + if #available(macOS 12, iOS 15.0, tvOS 15, watchOS 8, *) { + return try await self.data(for: request, delegate: nil) + } else { + return try await _data(for: request) + } + #else + return try await _data(for: request) + #endif + } +} + +internal extension URLSession { + + func _data(for request: URLRequest) async throws -> (Data, URLResponse) { + try await withCheckedThrowingContinuation { continuation in + let task = self.dataTask(with: request) { data, response, error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: (data ?? .init(), response!)) + } + } + task.resume() + } + } +} From b5b50c810fe58c399e49bd182a8d0e8e3e522d25 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 14 Dec 2024 03:22:59 +0000 Subject: [PATCH 05/12] Add GitHub CI --- .github/workflows/swift.yml | 48 +++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/swift.yml diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml new file mode 100644 index 0000000..db0e9e2 --- /dev/null +++ b/.github/workflows/swift.yml @@ -0,0 +1,48 @@ +name: Swift +on: [push] +jobs: + + macos: + name: macOS + runs-on: macos-latest + steps: + - name: Install Swift + uses: slashmo/install-swift@v0.3.0 + with: + version: 6.0.3 + - name: Checkout + uses: actions/checkout@v2 + - name: Swift Version + run: swift --version + - name: Install dependencies + run: brew install cairo fontconfig freetype + - name: Build (Debug) + run: swift build -c debug + - name: Build (Release) + run: swift build -c release + - name: Test (Debug) + run: swift test -c debug + + linux: + name: Linux + strategy: + matrix: + swift: [6.0.3] + runs-on: ubuntu-20.04 + steps: + - name: Install Swift + uses: slashmo/install-swift@v0.3.0 + with: + version: ${{ matrix.swift }} + - name: Checkout + uses: actions/checkout@v2 + - name: Swift Version + run: swift --version + - name: Install dependencies + run: sudo apt-get install -y libcairo2-dev libfreetype6-dev libfontconfig-dev + - name: Build (Debug) + run: swift build -c debug + - name: Build (Release) + run: swift build -c release + - name: Test (Debug) + run: swift test -c debug From 26101ba37ec77b35b70868c0c7150a3a0a658dac Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 14 Dec 2024 03:24:36 +0000 Subject: [PATCH 06/12] Update README --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ccff868..e7db379 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # Silica -Pure Swift CoreGraphics (or Quartz2D) implementation (Supports Linux) +Pure Swift CoreGraphics (Quartz2D) implementation + +This library is a compatibility layer over Cairo for porting UIKit (and CoreGraphics) drawing code to other platforms. \ No newline at end of file From 6ba24e3bbad5282493cd689c5caa2062ec4625ec Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 13 Dec 2024 22:51:35 -0500 Subject: [PATCH 07/12] Add typed error throwing --- Sources/Silica/CGContext.swift | 33 ++++++++++----------------------- Sources/Silica/CGFont.swift | 21 ++++++++++++++++----- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/Sources/Silica/CGContext.swift b/Sources/Silica/CGContext.swift index 40c997a..e4a8bd0 100644 --- a/Sources/Silica/CGContext.swift +++ b/Sources/Silica/CGContext.swift @@ -34,12 +34,11 @@ public final class CGContext { // MARK: - Initialization - public init(surface: Cairo.Surface, size: CGSize) throws { + public init(surface: Cairo.Surface, size: CGSize) throws(CairoError) { let context = Cairo.Context(surface: surface) - if let error = context.status.toError() { - + if let error = CairoError(context.status) { throw error } @@ -55,7 +54,6 @@ public final class CGContext { /// Returns the current transformation matrix. public var currentTransform: CGAffineTransform { - return CGAffineTransform(cairo: internalContext.matrix) } @@ -313,37 +311,28 @@ public final class CGContext { // MARK: Saving and Restoring the Graphics State - public func save() throws { - + public func save() throws(CairoError) { internalContext.save() - - if let error = internalContext.status.toError() { - + if let error = CairoError(internalContext.status) { throw error } - let newState = internalState.copy - newState.next = internalState - internalState = newState } - @inline(__always) - public func saveGState() { - + internal func saveGState() { try! save() } - public func restore() throws { + public func restore() throws(CairoError) { guard let restoredState = internalState.next - else { throw CAIRO_STATUS_INVALID_RESTORE.toError()! } + else { throw .invalidRestore } internalContext.restore() - if let error = internalContext.status.toError() { - + if let error = CairoError(internalContext.status) { throw error } @@ -354,7 +343,6 @@ public final class CGContext { @inline(__always) public func restoreGState() { - try! restore() } @@ -805,7 +793,7 @@ public final class CGContext { // MARK: - Private Functions - private func fillPath(evenOdd: Bool, preserve: Bool) throws { + private func fillPath(evenOdd: Bool, preserve: Bool) throws(CairoError) { if internalState.shadow != nil { @@ -828,8 +816,7 @@ public final class CGContext { endShadow() } - if let error = internalContext.status.toError() { - + if let error = CairoError(internalContext.status) { throw error } } diff --git a/Sources/Silica/CGFont.swift b/Sources/Silica/CGFont.swift index 7407982..12c7b77 100644 --- a/Sources/Silica/CGFont.swift +++ b/Sources/Silica/CGFont.swift @@ -54,12 +54,23 @@ public struct CGFont { self.name = name self.family = family - self.scaledFont = ScaledFont(face: face, matrix: Matrix.identity, currentTransformation: Matrix.identity, options: options) + do { + self.scaledFont = try ScaledFont( + face: face, + matrix: .identity, + currentTransformation: .identity, + options: options + ) + } + catch .noMemory { + assertionFailure("Insufficient memory to perform the operation.") + return nil + } + catch { + return nil + } - // Default font is Verdana, make sure the name is correct - let defaultFontName = "Verdana" - - guard name == defaultFontName || scaledFont.fullName != defaultFontName + guard name == scaledFont.fullName else { return nil } // cache From 14de123b69a4212d5e9b5bf07ebd32fbe59d212a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 13 Dec 2024 22:51:42 -0500 Subject: [PATCH 08/12] Update unit tests --- Tests/SilicaTests/FontTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/SilicaTests/FontTests.swift b/Tests/SilicaTests/FontTests.swift index d0381bf..753f18e 100644 --- a/Tests/SilicaTests/FontTests.swift +++ b/Tests/SilicaTests/FontTests.swift @@ -36,8 +36,8 @@ final class FontTests: XCTestCase { guard let font = Silica.CGFont(name: fontName) else { XCTFail("Could not create font \(fontName)"); return } - XCTAssert(font.name == font.name) - XCTAssert(expectedFullName == font.scaledFont.fullName, "\(expectedFullName) == \(font.scaledFont.fullName)") + XCTAssertEqual(font.name, font.name) + XCTAssertEqual(expectedFullName, font.scaledFont.fullName) } } } From 4387a9e7d2be2d16ac8f4a88c176ae7bd0afda21 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 13 Dec 2024 22:52:33 -0500 Subject: [PATCH 09/12] Ignore SwiftPM files --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e08d4a9..cda65c8 100644 --- a/.gitignore +++ b/.gitignore @@ -36,8 +36,8 @@ playground.xcworkspace # Packages/ Package.pins +Package.resolved .build/ -Bluetooth.xcodeproj/* .swiftpm # CocoaPods From 0e1f34fb8ccb7f7d2ec37ae30603f72717a94cfe Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 14 Dec 2024 00:47:19 -0500 Subject: [PATCH 10/12] Fixed font name parsing --- Sources/Silica/CGFont.swift | 39 ++++++++++--------------------------- 1 file changed, 10 insertions(+), 29 deletions(-) diff --git a/Sources/Silica/CGFont.swift b/Sources/Silica/CGFont.swift index 12c7b77..0b05ee3 100644 --- a/Sources/Silica/CGFont.swift +++ b/Sources/Silica/CGFont.swift @@ -70,7 +70,10 @@ public struct CGFont { return nil } - guard name == scaledFont.fullName + // Default font is Verdana, make sure the name is correct + let defaultFontName = "Verdana" + + guard name == defaultFontName || scaledFont.fullName != defaultFontName else { return nil } // cache @@ -170,29 +173,18 @@ internal func FcPattern(name: String) -> (pointer: OpaquePointer, family: String let cleanup = ErrorCleanup(cleanup: { FcPatternDestroy(pattern) }) - let separator = "-".withCString { (pointer) in return pointer[0] } + let separator: Character = "-" let traits: String? let family: String - if let traitsCString = strchr(name, CInt(separator)) { - - let trimmedCString = traitsCString.advanced(by: 1) - - // should free memory, but crashes - // defer { free(traitsCString) } - - let traitsString = String(cString: trimmedCString) - - let familyLength = name.utf8.count - traitsString.utf8.count - 1 // for separator - - family = name.substring(range: 0 ..< familyLength)! - - traits = traitsString - + let components = name.split(separator: separator, maxSplits: 2, omittingEmptySubsequences: true) + + if components.count == 2 { + family = String(components[0]) + traits = String(components[1]) } else { - family = name traits = nil } @@ -245,14 +237,3 @@ internal func FcPattern(name: String) -> (pointer: OpaquePointer, family: String return (pattern, family) } - -// MARK: - String Extensions - -internal extension String { - - func substring(range: Range) -> String? { - let indexRange = utf8.index(utf8.startIndex, offsetBy: range.lowerBound) ..< utf8.index(utf8.startIndex, offsetBy: range.upperBound) - let substring = String(utf8[indexRange]) - return substring - } -} From 40c89109ec798bbd834bb9dbf2ee02a87eb6411f Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 14 Dec 2024 02:17:50 -0500 Subject: [PATCH 11/12] Updated dependencies --- Package.swift | 28 ++++++-- Sources/Silica/CGFont.swift | 125 +++++++++++++----------------------- 2 files changed, 69 insertions(+), 84 deletions(-) diff --git a/Package.swift b/Package.swift index df334d0..d3481f1 100644 --- a/Package.swift +++ b/Package.swift @@ -4,14 +4,32 @@ import PackageDescription let package = Package( name: "Silica", products: [ - .library(name: "Silica", targets: ["Silica"]) + .library( + name: "Silica", + targets: ["Silica"] + ) ], dependencies: [ - .package(url: "https://github.com/PureSwift/Cairo.git", .branch("master")) + .package( + url: "https://github.com/PureSwift/Cairo.git", + branch: "master" + ), + .package( + url: "https://github.com/PureSwift/FontConfig.git", + branch: "master" + ) ], targets: [ - .target(name: "Silica", dependencies: ["Cairo"]), - .testTarget(name: "SilicaTests", dependencies: ["Silica"]) + .target( + name: "Silica", + dependencies: [ + "Cairo", + "FontConfig" + ] + ), + .testTarget( + name: "SilicaTests", + dependencies: ["Silica"] + ) ] - ) diff --git a/Sources/Silica/CGFont.swift b/Sources/Silica/CGFont.swift index 0b05ee3..1b3abf2 100644 --- a/Sources/Silica/CGFont.swift +++ b/Sources/Silica/CGFont.swift @@ -8,7 +8,7 @@ import Cairo import CCairo -import CFontConfig +import FontConfig import Foundation #if os(macOS) import struct CoreGraphics.CGAffineTransform @@ -34,7 +34,7 @@ public struct CGFont { // MARK: - Initialization - public init?(name: String) { + public init?(name: String, configuration: FontConfiguration = .current) { if let cachedFont = CGFont.cache[name] { @@ -43,10 +43,11 @@ public struct CGFont { } else { // create new font - guard let (fontConfigPattern, family) = FcPattern(name: name) + guard let pattern = FontConfig.Pattern(cgFont: name, configuration: configuration), + let family = pattern.family else { return nil } - let face = FontFace(fontConfigPattern: fontConfigPattern) + let face = FontFace(pattern: pattern) let options = FontOptions() options.hintMetrics = .off @@ -147,93 +148,59 @@ public extension CGFont { } } -// MARK: - Private +// MARK: - Font Config Pattern -/// Initialize a pointer to a `FcPattern` object created from the specified PostScript font name. -internal func FcPattern(name: String) -> (pointer: OpaquePointer, family: String)? { +internal extension FontConfig.Pattern { - guard let pattern = FcPatternCreate() - else { return nil } - - /// hacky way to cleanup, `defer` will copy initial value of `Bool` so this is needed. - /// ARC will cleanup for us - final class ErrorCleanup { + convenience init?(cgFont name: String, configuration: FontConfiguration = .current) { + self.init() - var shouldCleanup = true + let separator: Character = "-" - let cleanup: () -> () + let traits: String? - deinit { if shouldCleanup { cleanup() } } + let family: String - init(cleanup: @escaping () -> ()) { - - self.cleanup = cleanup - } - } - - let cleanup = ErrorCleanup(cleanup: { FcPatternDestroy(pattern) }) - - let separator: Character = "-" - - let traits: String? - - let family: String - - let components = name.split(separator: separator, maxSplits: 2, omittingEmptySubsequences: true) - - if components.count == 2 { - family = String(components[0]) - traits = String(components[1]) - } else { - family = name - traits = nil - } - - guard FcPatternAddString(pattern, FC_FAMILY, family) != 0 - else { return nil } - - // FontConfig assumes Medium Roman Regular, add / replace additional traits - if let traits = traits { + let components = name.split(separator: separator, maxSplits: 2, omittingEmptySubsequences: true) - if traits.contains("Bold") { - - guard FcPatternAddInteger(pattern, FC_WEIGHT, FC_WEIGHT_BOLD) != 0 - else { return nil } + if components.count == 2 { + family = String(components[0]) + traits = String(components[1]) + } else { + family = name + traits = nil } - if traits.contains("Italic") { - - guard FcPatternAddInteger(pattern, FC_SLANT, FC_SLANT_ITALIC) != 0 - else { return nil } - } + self.family = family + assert(self.family == family) - if traits.contains("Oblique") { + // FontConfig assumes Medium Roman Regular, add / replace additional traits + if let traits = traits { - guard FcPatternAddInteger(pattern, FC_SLANT, FC_SLANT_OBLIQUE) != 0 - else { return nil } - } - - if traits.contains("Condensed") { + if traits.contains("Bold") { + self.weight = .bold + } - guard FcPatternAddInteger(pattern, FC_WIDTH, FC_WIDTH_CONDENSED) != 0 - else { return nil } + if traits.contains("Italic") { + self.slant = .italic + } + + if traits.contains("Oblique") { + self.slant = .oblique + } + + if traits.contains("Condensed") { + self.width = .condensed + } } + + guard configuration.substitute(pattern: self, kind: FcMatchPattern) + else { return nil } + + self.defaultSubstitutions() + + guard configuration.match(self) != nil + else { return nil } + } - - let matchPattern = FcMatchKind(rawValue: 0) // FcMatchPattern - - guard FcConfigSubstitute(nil, pattern, matchPattern) != 0 - else { return nil } - - FcDefaultSubstitute(pattern) - - var result = FcResult(0) - - guard FcFontMatch(nil, pattern, &result) != nil - else { return nil } - - // success - cleanup.shouldCleanup = false - - return (pattern, family) } From e5affd89aadb9e206f26b857eacf89aeeee9a132 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Sat, 14 Dec 2024 02:17:56 -0500 Subject: [PATCH 12/12] Updated unit tests --- Tests/SilicaTests/FontTests.swift | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Tests/SilicaTests/FontTests.swift b/Tests/SilicaTests/FontTests.swift index 753f18e..7f0b80b 100644 --- a/Tests/SilicaTests/FontTests.swift +++ b/Tests/SilicaTests/FontTests.swift @@ -8,6 +8,7 @@ import XCTest @testable import Silica +import FontConfig final class FontTests: XCTestCase { @@ -15,29 +16,32 @@ final class FontTests: XCTestCase { #if os(Linux) var fontNames = [ - ("LiberationSerif", "Liberation Serif"), - ("LiberationSerif-Bold", "Liberation Serif") + ("LiberationSerif", "Liberation Serif", FontWeight.regular), + ("LiberationSerif-Bold", "Liberation Serif", .bold) ] #else var fontNames = [ - ("TimesNewRoman", "Times New Roman"), - ("TimesNewRoman-Bold", "Times New Roman") + ("TimesNewRoman", "Times New Roman", FontWeight.regular), + ("TimesNewRoman-Bold", "Times New Roman", .bold) ] #endif #if os(macOS) - fontNames += [("MicrosoftSansSerif", "Microsoft Sans Serif"), - ("MicrosoftSansSerif-Bold", "Microsoft Sans Serif")] + fontNames += [("MicrosoftSansSerif", "Microsoft Sans Serif", .regular), + ("MicrosoftSansSerif-Bold", "Microsoft Sans Serif", .bold)] #endif - for (fontName, expectedFullName) in fontNames { + for (fontName, expectedFullName, weight) in fontNames { - guard let font = Silica.CGFont(name: fontName) + guard let font = Silica.CGFont(name: fontName), + let pattern = FontConfig.Pattern(cgFont: fontName) else { XCTFail("Could not create font \(fontName)"); return } XCTAssertEqual(font.name, font.name) XCTAssertEqual(expectedFullName, font.scaledFont.fullName) + XCTAssertEqual(pattern.family, font.family) + XCTAssertEqual(pattern.weight, weight) } } }