diff --git a/.github/actions/build-docs/action.yml b/.github/actions/build-docs/action.yml new file mode 100644 index 0000000..9688ab4 --- /dev/null +++ b/.github/actions/build-docs/action.yml @@ -0,0 +1,20 @@ +name: 'Build Docs' + +inputs: + asdf-system: + description: 'ASDF system to build system for' + required: true + qlfile-template: + description: "Djula template for qlfile. All environment variables are available in it's context" + required: false + +runs: + using: composite + steps: + - uses: 40ants/cl-info/.github/actions/setup-lisp@custom-action + with: + asdf-system: cl-info + - name: Finish + shell: bash + run: | + echo "DONE" diff --git a/.github/actions/setup-lisp/action.yml b/.github/actions/setup-lisp/action.yml new file mode 100644 index 0000000..44bade2 --- /dev/null +++ b/.github/actions/setup-lisp/action.yml @@ -0,0 +1,76 @@ +name: 'Setup Common Lisp' + +inputs: + asdf-system: + description: 'ASDF system to install' + required: true + qlfile-template: + description: "Djula template for qlfile. All environment variables are available in it's context" + required: false + +runs: + using: composite + steps: + - name: Current Env + shell: bash + run: | + echo ::group::Environment + echo "Current dir:" + pwd + + echo "Environment Variables:" + env | sort -u + echo ::endgroup:: + - name: Install Roswell + shell: bash + run: | + echo ::group::Install Roswell + if [[ "$OS" == "ubuntu-latest" ]]; then + sudo apt-get -y install git build-essential automake libcurl4-openssl-dev + fi + if [[ "$OS" == "macos-latest" ]]; then + brew install automake autoconf curl + fi + + curl -L https://raw.githubusercontent.com/svetlyak40wt/roswell/patches/scripts/install-for-ci.sh | sh + + echo $HOME/.roswell/bin >> $GITHUB_PATH + echo ::endgroup:: + - name: Upgrade ASDF to the Latest Version + shell: bash + run: | + echo ::group::Upgrade ASDF + ros install asdf + echo ::endgroup:: + - name: Install Qlot + shell: bash + run: | + echo ::group::Install Qlot + ros install qlot + echo .qlot/bin >> $GITHUB_PATH + echo ::endgroup:: + + - name: Create Qlot Environment + shell: bash + run: | + echo ::group::Create Qlot Environment + + if [[ -n "${QLFILE_TEMPLATE}" ]]; then + echo "${QLFILE_TEMPLATE}" | ${{ github.action_path }}/templater.ros > qlfile + rm -f qlfile.lock + fi + + qlot install + echo ::endgroup:: + env: + QLFILE_TEMPLATE: ${{ inputs.qlfile-template }} + + # This step will install system and + # all possible roswell scripts, if the system + # has them in the roswell/ subdirectory: + - name: Install ASDF System + shell: bash + run: | + echo ::group::Install ASDF System + qlot exec ros install ${{ inputs.asdf-system }} + echo ::endgroup:: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..db0f14f --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,28 @@ +name: 'Docs' + +on: + # This will run tests on pushes + # to master branch or on pull merges: + push: + branches: + - 'main' + - 'master' + # This will run tests for every pull request: + pull_request: + # Rerun tests at 10 AM every Monday + # to check if they still work with latest dependencies. + schedule: + - cron: '0 10 * * 1' + +jobs: + build-docs: + runs-on: ubuntu-latest + + env: + LISP: sbcl-bin + + steps: + - uses: actions/checkout@v1 + - uses: 40ants/cl-info/.github/actions/build-docs@custom-action + with: + asdf-system: cl-info diff --git a/cl-info-docs.asd b/cl-info-docs.asd new file mode 100644 index 0000000..57befb9 --- /dev/null +++ b/cl-info-docs.asd @@ -0,0 +1,6 @@ +(defsystem example-docs + :build-operation build-docs-op + :build-pathname "docs/build/" + :class :package-inferred-system + :pathname "docs/source/" + :depends-on ("example-docs/docs")) diff --git a/docs/scripts/build.ros b/docs/scripts/build.ros new file mode 100755 index 0000000..fb1df80 --- /dev/null +++ b/docs/scripts/build.ros @@ -0,0 +1,163 @@ +#!/bin/sh +#|-*- mode:lisp -*-|# +#| +exec ros -Q -- $0 "$@" +|# +(progn ;;init forms + (ros:ensure-asdf) + #+quicklisp + (ql:quickload '(log4cl + example-docs) + :silent t)) + +(defpackage :script.build-docs + (:use :cl)) +(in-package :script.build-docs) + + +(define-condition unable-to-proceed (simple-error) + ((message :initarg :message + :reader get-message)) + (:report (lambda (condition stream) + (format stream (get-message condition))))) + + +(define-condition subprocess-error-with-output (uiop::subprocess-error) + ((stdout :initarg :stdout :reader subprocess-error-stdout) + (stderr :initarg :stderr :reader subprocess-error-stderr)) + (:report (lambda (condition stream) + (format stream "Subprocess ~@[~S~% ~]~@[with command ~S~% ~]exited with error~@[ code ~D ~]~@[ and this text at stderr:~% ~S~]" + (uiop:subprocess-error-process condition) + (uiop:subprocess-error-command condition) + (uiop:subprocess-error-code condition) + (subprocess-error-stderr condition)) + ))) + +(defun run (command &key (raise t)) + "Runs command and returns it's stdout stderr and code. + +If there was an error, raises subprocess-error-with-output, but this +behaviour could be overriden by keyword argument ``:raise t``." + + (multiple-value-bind (stdout stderr code) + (uiop:run-program command + :output '(:string :stripped t) + :error-output '(:string :stripped t) + :ignore-error-status t) + + (when (and raise + (not (eql code 0))) + (error 'subprocess-error-with-output + :stdout stdout + :stderr stderr + :code code + :command command)) + (values stdout stderr code))) + + +(defun build-docs () + (log:info "Building documentation in ./docs/") + + (example-docs:build-docs) + + (uiop:with-output-file (s "docs/build/.nojekyll" :if-exists :overwrite) + (declare (ignorable s)))) + + +(defun gh-pages-repository-initialized-p () + "Checks if repository for documentation already initialized" + (uiop:directory-exists-p "docs/build/.git")) + + +(defun git (&rest commands) + "Calls git command in gh-pages repository." + + (let ((directory "docs/build/")) + (uiop:with-current-directory (directory) + (let ((command (apply #'concatenate 'string + "git " + commands))) + + (log:info "Running" command "in" directory) + (run command))))) + + +(defun git-repository-was-changed-p () + ;; if git status returns something, then repository have uncommitted changes + (> (length (git "status --porcelain")) + 0)) + + +(defun get-git-upstream () + ;; taken from http://stackoverflow.com/a/9753364/70293 + (let ((upstream (run "git rev-parse --abbrev-ref --symbolic-full-name @{u}" :raise nil))) + (when (> (length upstream) + 0) + (subseq upstream + 0 + (search "/" upstream))))) + + +(defun get-origin-to-push () + (let ((upstream (get-git-upstream))) + + (cond + (upstream + ;; If there is already some remote upstream, then use it + (run (concatenate 'string "git remote get-url " upstream))) + ;; If we are running inside github actions + ((uiop:getenv "GITHUB_ACTIONS") + (unless (uiop:getenv "GITHUB_TOKEN") + (error 'unable-to-proceed + :message "Please, provide GITHUB_TOKEN environment variable.")) + (format nil "https://~A:~A@github.com/~A" + (uiop:getenv "GITHUB_ACTOR") + (uiop:getenv "GITHUB_TOKEN") + (uiop:getenv "GITHUB_REPOSITORY"))) + ;; otherwise make it from travis secret token and repo slug + (t + (let ((repo-slug (uiop:getenv "TRAVIS_REPO_SLUG")) + (repo-token (uiop:getenv "GH_REPO_TOKEN"))) + + (unless (and repo-slug repo-token) + (error 'unable-to-proceed + :message "Current branch does not track any upstream and there is no TRAVIS_REPO_SLUG and GH_REPO_TOKEN env variables. Where to push gh-pages branch?")) + + (format nil "https://~A@github.com/~A" + repo-token + repo-slug)))))) + + +(defun push-gh-pages () + (log:info "Pushing changes to gh-pages branch") + + (unless (gh-pages-repository-initialized-p) + (git "init") + + (git "remote add origin " + (get-origin-to-push))) + + (git "add .") + + (cond + ((git-repository-was-changed-p) + (when (uiop:getenv "GITHUB_ACTIONS") + (git "config --global user.name \"github-actions[bot]\"") + (git "config --global user.email \"actions@github.com\"")) + (git "commit -m 'Update docs'") + + (git "push --force origin master:gh-pages")) + ;; or + (t (log:info "Everything is up to date.")))) + + +(defun main (&rest argv) + (declare (ignorable argv)) + (log:config :debug) + (log:info "Building documentation") + + (handler-bind ((error (lambda (condition) + (uiop:print-condition-backtrace condition :stream *error-output*) + (uiop:quit 1)))) + (build-docs) + (push-gh-pages))) diff --git a/docs/source/docs.lisp b/docs/source/docs.lisp new file mode 100644 index 0000000..907f889 --- /dev/null +++ b/docs/source/docs.lisp @@ -0,0 +1,168 @@ +(defpackage #:example-docs/docs + (:nicknames #:example-docs) + (:use #:cl) + (:import-from #:mgl-pax + #:section + #:defsection) + (:import-from #:example/app + #:@app) + (:import-from #:example/utils + #:@utils) + (:export + #:build-docs)) +(in-package example-docs/docs) + + +(defsection @index (:title "Example of MGL-PAX Common Lisp Documentation Builder") + " +This is small library includes a few functions with docstrings and a documentation +for the system and all included packages. + +The purpose is to demonstrate core features of the +[MGL-PAX](http://melisgl.github.io/mgl-pax/) documentation builder. + +This repository is part of the organization, +created to compare different Common Lisp documentation systems. + +The goal is make it easier for CL software developers to choose proper +documentation system and to improve docs in their software! + +Resulting documentation can be viewed here: + + + +The repository can be used as a template for new libraries if you've choosen `MGL-PAX` +for documenting your library. + +Let's review features, provided by `MGL-PAX`. + +" + (@pros-n-cons section) + (@how-to-build section) + (@handwritten section) + (@autogenerated section) + (@packages section)) + + +(defsection @pros-n-cons (:title "Pros & Cons of PAX") + (@pros section) + (@cons section)) + + +(defsection @pros (:title "Pros") + " +* Markdown is widely used markup format and PAX uses it everywhere. +* Cross-referencing works like a charm and you can reference different + types of objects using [Locatives](http://melisgl.github.io/mgl-pax/#x-28MGL-PAX-3A-40MGL-PAX-LOCATIVES-AND-REFERENCES-20MGL-PAX-3ASECTION-29). +* New types of documentation objects can be defined in Common Lisp using CLOS. + Here is [an example](http://melisgl.github.io/mgl-pax/#x-28MGL-PAX-3AREFERENCE-LOCATIVE-20-28MGL-PAX-3AREADER-20MGL-PAX-3AREFERENCE-29-29). +* Emacs/SLIME integration and ability to jump to a xref objects using M-. +* Ability to generate Markdown README files as well as HTML. +* It is possible to link documentation and sources on the GitHub. +* Docstrings deindentation allows to format code nicely. +* You can generation docs for a multiple ASDF systems with cross-referencing. +* Autoexports all documented symbols. No need to enumerate them in `defpackage` form. +* There is a nice default HTML theme. +* No external tools like Sphinx. Everything is in Common Lisp. +") + + +(defsection @cons (:title "Cons") + " +* Markdown format may be somewhat limited and probably it can be non-trivial or not possible + to extend it in some rare cases. +* The recommended way to mix documentation section with code leads to + the runtime dependency from PAX and all it's dependencies. But you + might define documentation as a separate ASDF system. +* PAX does not notifies you if some references are missing or there are unused sections. + These mistakes can be catched automatically. +* It is inconvenient to write Markdown in docstrings. Is there any way + to teach Emacs to use markdown minor mode for documentation strings? +* There is no magically autogenerated reference API. See @AUTOGENERATED. + + But if you prefer another way, it should be easy to write a function which + will collect external symbols and generate a MGL-PAX:SECTION object for them. +") + + +(defsection @how-to-build (:title "How to build the documentation") + " +MGL-PAX has a number of ways for generation of the docs. But most probably, +you'll need only toplevel helpers described in it's section +[Generating Documentation](http://melisgl.github.io/mgl-pax/#toc-7-generating-documentation). + +These helpers is able to generate README and HTML docs for an ASDF system. + +This example defines it's own wrapper EXAMPLE-DOCS:BUILD-DOCS: +" + (build-docs function) + + " +It is as simple, as: + + +``` +(defun build-docs () + (mgl-pax:update-asdf-system-readmes @index :example) + + (mgl-pax:update-asdf-system-html-docs @index :example + :target-dir \"docs/build/\")) +``` + +This function is used by docs/scripts/build.ros file to generate documentation from GitHub Actions. +Or can be called from the REPL. +") + + +(defsection @handwritten (:title "Handwritten Documentation") + " +I think the ability to write a large pieces of documentation which aren't bound to +a function, class or module is an important feature. This way you can tell the user +about some toplevel abstractions and give a bird eye view on the library or system. + +For example, handwritten parts of the documentation can provide some code snippets +to demonstrate the ways, how to use the library: + +``` +(loop repeat 42 + collect (foo \"bar\" 100500)) +``` + +And when you are talking about some function or class, you can reference it. +For example, if I'm talking about the FOO function, I can reference it like this +`[example/app:foo][function]` and it will appear in the code as +the link [example/app:foo]. In most cases you can omit square brakets and just +capitalize symbol name. + +Comparing MGL-PAX to Coo (here is it's [example project](https://cl-doc-systems.github.io/coo/), +I'd prefer the PAX, because it's ability to mix handwriten sections with documentation extracted +from docstrings. + +") + + +(defsection @autogenerated (:title "Autogenerated API Reference") + " +There is no magically autogenerated reference API. Idea of PAX is that you +write docs manually and reference documented symbols. They are automatically +exported and this way you library's external API should be documented. + +But if you prefer another way, it should be easy to write a function which +will collect external symbols and generate a MGL-PAX:SECTION object for them. +") + +(defsection @packages (:title "Packages") + (@app section) + (@utils section)) + + +(defun build-docs () + (mgl-pax:update-asdf-system-readmes @index :example) + + (mgl-pax:update-asdf-system-html-docs + @index :example + :target-dir "docs/build/" + :pages `((:objects (,example-docs:@index) + :source-uri-fn ,(pax:make-github-source-uri-fn + :example + "https://github.com/cl-doc-systems/mgl-pax"))))) diff --git a/src/core.lisp b/src/core.lisp index 1dc254c..6cf43e0 100644 --- a/src/core.lisp +++ b/src/core.lisp @@ -1,27 +1,103 @@ -(defpackage #:cl-info/core - (:nicknames #:cl-info) +(defpackage #:cl-info + (:nicknames #:cl-info/core) (:use #:cl) - (:export - #:cl-info - #:get-asdf-version - #:get-lisp-type - #:get-lisp-version - #:get-software-type - #:get-software-version - #:get-ql-dists - #:get-cl-info - #:system-info - #:get-name - #:get-version - #:get-path - #:absent-p - #:get-system-info)) + (:import-from #:mgl-pax-minimal + #:defsection + #:reader) + (:export #:cl-info + #:get-cl-info + #:get-system-info)) (in-package cl-info/core) +(defsection @index (:title "CL-INFO - Common Lisp Environment Reporter") + " +[![](https://github-actions.40ants.com/40ants/cl-info/matrix.svg)](https://github.com/40ants/cl-info/actions) + +This is a small utility, useful to display information about you Common +Lisp environment. You might want to call it in the CI pipeline or +to use it when rendering a crash report in some client applications. + +Usage from Common Lisp +====================== + +It's main entry point is CL-INFO:GET-CL-INFO function. It returns an object with +customized PRINT-OBJECT method. You can use it to output debug +information in your programs. + +CL-INFO collects inforrmation about OS, Lisp Implementation, ASDF, installed +Quicklisp distributions: + + CL-USER> (cl-info:get-cl-info) + OS: Darwin 15.6.0 + Lisp: SBCL 1.4.8 + ASDF: 3.3.1.1 + QL: ceramic github-e0d905187946f8f2358f7b05e1ce87b302e34312 + cl-prevalence github-c163c227ed85d430b82cb1e3502f72d4f88e3cfa + log4cl-json github-c4f786e252d89a45372186aaf32fb8e8736b444b + log4cl github-6a857b0b41c030a8a3b04096205e221baaa1755f + quicklisp 2018-04-30 + slynk github-3314cf8c3021cb758e0e30fe3ece54accf1dcf3d + weblocks-lass github-1b043afbf2f3e84e495dfeae5e63fe67a435019f + weblocks-parenscript github-8ef4ca2f837403a05c4e9b92dcf1c41771d16f17 + weblocks-ui github-5afdf238534d70edc2447d0bc8bc63da8e35999f + weblocks-websocket github-b098db7f179dce3bfb045afd4e35e7cc868893f0 + weblocks github-282483f97d6ca351265ebfebb017867c295d01ad + websocket-driver github-a3046b11dfb9803ac3bff7734dd017390c2b54bb + CL-USER> + +Also, you can gather information about separate systems using CL-INFO:GET-SYSTEM-INFO +function: + + CL-USER> (cl-info:get-system-info :hamcrest) + System: HAMCREST 0.4.2 + /Users/art/common-lisp/cl-hamcrest/src/ + + +Usage From Command-line +======================= + +Also, you can use CL-INFO as a command-line utility. It is useful to +output information about common lisp environment running on CI server. + +Here is how to do it: + +```shell +# Here we use a Roswell, to install utility +[art@art-osx:~]% ros install 40ants/cl-info + +# And now request information about lisp and some systems +[art@art-osx:~]% cl-info weblocks clack jonathan some-other-system +OS: Darwin 15.6.0 +Lisp: Clozure Common Lisp Version 1.11.5/v1.11.5 (DarwinX8664) +ASDF: 3.3.1.1 +QL: org.borodust.bodge 20180214223017 + quicklisp 2017-10-23 +System: weblocks 0.31.1 + /Users/art/common-lisp/weblocks/src/ +System: clack 2.0.0 + /Users/art/common-lisp/clack/ +System: jonathan 0.1 + /Users/art/.roswell/lisp/quicklisp/dists/quicklisp/software/jonathan-20170630-git/ +System: some-other-system is not available +``` + +API Reference +============= +" + (get-cl-info function) + (get-system-info function) + + (cl-info class) + (get-asdf-version (reader cl-info)) + + (system-info class)) + + (defclass cl-info () ((asdf-version :initform (asdf:asdf-version) - :reader get-asdf-version) + :reader get-asdf-version + :documentation "Returns ASDF version.") (lisp-type :initform (lisp-implementation-type) :reader get-lisp-type) (lisp-version :initform (lisp-implementation-version)