diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4b818f2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,20 @@ +name: CI + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + clairnote: ["sn", "dn"] + lilypond: ["2-24", "2-25"] + + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/nix-installer-action@main + - uses: DeterminateSystems/magic-nix-cache-action@main + - run: CLAIRNOTE=${{ matrix.clairnote }} ./scripts/test.py -o out -l "$(nix build .#lilypond-${{ matrix.lilypond }} --quiet --no-link --print-out-paths)/bin/lilypond" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf5933d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pdf +*.midi diff --git a/clairnote.ly b/clairnote.ly index 09f3c2b..6e99381 100644 --- a/clairnote.ly +++ b/clairnote.ly @@ -27,88 +27,14 @@ % For docstrings we use ;; instead of the usual "" to allow automated % minification for LilyBin + Clairnote. -%--- UTILITY FUNCTIONS ---------------- - -#(define (non-zero? n) (not (zero? n))) - -#(define (positive-integer? n) (and (positive? n) (integer? n))) - -#(define (map-pair proc pair) - (cons - (proc (car pair)) - (proc (cdr pair)))) - -#(define (cn-pitch-to-semitone pitch) - ;; Takes a pitch object and returns a semitone integer that corresponds to - ;; the pitch's position on the Clairnote staff. Used for - ;; staffLineLayoutFunction. The return value is almost always the semitone - ;; returned by (ly:pitch-semitones pitch) except for quarter tone - ;; alteration exceptions. 1/4 and 3/4 alterations are quarter tone sharps - ;; and their semitone needs to be adjusted down by one. - (let - ((alteration (ly:pitch-alteration pitch)) - (semitone (ly:pitch-semitones pitch))) - (cond - ((= 1/4 alteration) (- semitone 1)) - ((= 3/4 alteration) (- semitone 1)) - (else semitone)))) - -#(define (cn-notehead-pitch grob) - ;; Takes a note head grob and returns its pitch. - (define event (ly:grob-property grob 'cause)) - (if (ly:stream-event? event) - (ly:event-property event 'pitch) - (begin - (ly:warning "clairnote.ly cannot access the pitch of a note head grob. (Are you trying to use the Ambitus_engraver? It is incompatible with clairnote.ly.)") - (ly:make-pitch 0 0 0)))) - -#(define (cn-notehead-semitone grob) - ;; Takes a note head grob and returns its semitone. - (cn-pitch-to-semitone (cn-notehead-pitch grob))) - -#(define (cn-staff-symbol-property grob prop default) - ;; Takes a grob @var{grob}, a symbol @var{prop}, and - ;; a @var{default} value. Returns that custom StaffSymbol - ;; property or silently falls back to the default value. - (define staff-sym (ly:grob-object grob 'staff-symbol)) - (if (ly:grob? staff-sym) - (ly:grob-property staff-sym prop) - default)) - -#(define (cn-get-base-staff-space grob) - ;; Takes a grob and returns the custom StaffSymbol property - ;; cn-base-staff-space. Silently falls back to the default of 0.75. - (cn-staff-symbol-property grob 'cn-base-staff-space 0.75)) - -#(define (cn-magnification grob) - ;; Return the current magnification (from magnifyStaff, etc.) - ;; via a grob's font size. - (magstep (ly:grob-property grob 'font-size 0))) - -#(define (cn-get-staff-clef-adjust staff-octaves clef-octave-shift) - ;; Calculate the amount to vertically adjust the position of the clef, - ;; key signature, and time signature, in note-spaces / half-staff-spaces. - (+ - (* 12 clef-octave-shift) - (if (odd? staff-octaves) - 6 - (if (> staff-octaves 2) 12 0)))) - -#(define (cn-staff-clef-adjust-from-grob grob) - (cn-get-staff-clef-adjust - (cn-staff-symbol-property grob 'cn-staff-octaves 2) - (cn-staff-symbol-property grob 'cn-clef-shift 0))) - -#(define (cn-note-heads-from-grob grob default) - ;; Takes a grob like a Stem and returns a list of - ;; NoteHead grobs or default. - (let* ((heads-array (ly:grob-object grob 'note-heads)) - (heads-list (if (ly:grob-array? heads-array) - (ly:grob-array->list heads-array) - ;; should never/rarely? happen: - default))) - heads-list)) +% https://www.gnu.org/software/guile/manual/html_node/Load-Paths.html +#(add-to-load-path (dirname (current-filename))) +#(load + (string-concatenate + (list + (dirname (current-filename)) + "/clairnote/init.scm"))) %--- NOTE HEADS AND STEM ATTACHMENT ---------------- @@ -139,7 +65,7 @@ #(define (cn-whole-note-stencil grob white-note) ;; Returns default Clairnote whole note stencils. - (let ((mag (cn-magnification grob)) + (let ((mag (cn:magnification grob)) (wn-path (if white-note cn-whole-note-white-path cn-whole-note-black-path))) @@ -192,7 +118,7 @@ ;; http://scripts.sil.org/OFL ;; http://www.smufl.org/fonts/ ;; http://blog.steinberg.net/2013/05/introducing-bravura-music-font/ - (let ((mag (cn-magnification grob)) + (let ((mag (cn:magnification grob)) (nh-path (if white-note cn-note-white-path cn-note-black-path))) @@ -344,7 +270,7 @@ (assoc-ref cn-alterations (cons octave notename))) (cn-extract-alteration alteration-def))) (pitch (ly:make-pitch octave notename alter)) - (semitone (cn-pitch-to-semitone pitch))) + (semitone (cn:pitch-to-semitone pitch))) (cons semitone alteration-def))) accidental-alterations)) @@ -435,7 +361,7 @@ (notename (ly:pitch-notename pitch)) (octave (ly:pitch-octave pitch)) (alter (ly:pitch-alteration pitch)) - (semitone (cn-pitch-to-semitone pitch)) + (semitone (cn:pitch-to-semitone pitch)) ;; will be #f or (alter barnum . end-moment) (from-cn-semi-alterations (assoc-get semitone cn-semi-alterations)) @@ -518,7 +444,7 @@ accidental-styles.none = #'(#t () ()) (make-engraver (acknowledgers ((accidental-interface engraver grob source-engraver) - (let* ((pitch (cn-notehead-pitch (ly:grob-parent grob Y))) + (let* ((pitch (cn:notehead-pitch (ly:grob-parent grob Y))) (note (ly:pitch-notename pitch)) (key-alterations (ly:context-property context 'keyAlterations '())) (key-sig-alt (assoc-ref key-alterations note)) @@ -582,7 +508,7 @@ accidental-styles.none = #'(#t () ()) #(define (cn-accidental-grob-callback grob) ;; Returns an accidental sign stencil. - (let* ((mag (cn-magnification grob)) + (let* ((mag (cn:magnification grob)) (alt (accidental-interface::calc-alteration grob)) (direction (and (= 0 alt) (ly:grob-property grob 'cn-natural-sign-direction))) @@ -699,12 +625,12 @@ accidental-styles.none = #'(#t () ()) #(define (cn-draw-keysig grob) ;; Draws Clairnote key signature stencils. (let* - ((base-staff-space (cn-get-base-staff-space grob)) + ((base-staff-space (cn:get-base-staff-space grob)) (tonic-pitch (ly:grob-property grob 'cn-tonic)) ;; number of the tonic (0-6) (C-B) (tonic-num (ly:pitch-notename tonic-pitch)) ;; semitone of tonic (0-11) (C-B) - (tonic-semi (modulo (cn-pitch-to-semitone tonic-pitch) 12)) + (tonic-semi (modulo (cn:pitch-to-semitone tonic-pitch) 12)) (alt-list (ly:grob-property grob 'alteration-alist)) (alt-count (cn-get-keysig-alt-count alt-list)) @@ -720,7 +646,7 @@ accidental-styles.none = #'(#t () ()) (base-vert-adj (if (= 0 tonic-semi) tonic-semi (- tonic-semi 12))) ;; adjust position for odd octave staves and clefs shifted up/down an octave, etc. - (staff-clef-adjust (cn-staff-clef-adjust-from-grob grob)) + (staff-clef-adjust (cn:staff-clef-adjust-from-grob grob)) (vert-adj (* note-space (+ base-vert-adj staff-clef-adjust))) (stack (ly:stencil-translate-axis raw-stack vert-adj Y))) @@ -741,7 +667,7 @@ accidental-styles.none = #'(#t () ()) (if (grob::has-interface grob 'key-cancellation-interface) #f (let ((stil (cn-draw-keysig grob)) - (mag (cn-magnification grob))) + (mag (cn:magnification grob))) (ly:stencil-scale stil mag mag)))) #(define (Cn_key_signature_engraver context) @@ -943,7 +869,7 @@ accidental-styles.none = #'(#t () ()) ;; up 6 note-positions for odd octave staves ;; up 12 for even octave staves with 4 or more octaves ;; up or down 12 * Staff.cnClefShift - ((clef-adjust (cn-get-staff-clef-adjust + ((clef-adjust (cn:get-staff-clef-adjust (ly:context-property context 'cnStaffOctaves) (ly:context-property context 'cnClefShift)))) @@ -1104,15 +1030,15 @@ accidental-styles.none = #'(#t () ()) (if curve-path (let* ((curve-stil (make-path-stencil curve-path 0.0001 1 1 #t)) - (mag (cn-magnification grob)) + (mag (cn:magnification grob)) (scaled-curve (ly:stencil-scale curve-stil mag mag)) (transpo (ly:grob-property grob 'cn-clef-transposition)) ;; bass clef default octave is 3, treble and alto are 4 (default-octave (if (string=? "clefs.F" glyph) 3 4)) (octave (+ default-octave (/ transpo 12))) - (number-shift (map-pair (lambda (x) (* x mag)) - (cn-clef-number-shift glyph octave))) + (number-shift (cn:map-pair (lambda (x) (* x mag)) + (cn-clef-number-shift glyph octave))) (scale 0.9) (number-stil (ly:stencil-translate (ly:stencil-scale @@ -1186,19 +1112,19 @@ accidental-styles.none = #'(#t () ()) #(define (cn-time-signature-grob-callback grob) ;; Adjust vertical position of time sig based on vertical staff scaling. (let* - ((base-staff-space (cn-get-base-staff-space grob)) + ((base-staff-space (cn:get-base-staff-space grob)) (vscale-staff (* 12/7 base-staff-space)) (base-y-offset (* (+ vscale-staff -0.9) -2.5)) ;; adjust position for odd octave staves and clefs shifted up/down an octave ;; note-space is the distance between two adjacent notes given vertical staff compression (note-space (* 0.5 base-staff-space)) - (staff-clef-adjust (cn-staff-clef-adjust-from-grob grob)) + (staff-clef-adjust (cn:staff-clef-adjust-from-grob grob)) (y-offset (+ base-y-offset (* note-space staff-clef-adjust))) ;; adjustment for \magnifyStaff - (mag (cn-magnification grob)) + (mag (cn:magnification grob)) (final-y-offset (* y-offset mag))) (ly:grob-set-property! grob 'Y-offset final-y-offset))) @@ -1263,7 +1189,7 @@ accidental-styles.none = #'(#t () ()) ;; --- Y / length --- (stem-y-extent (ly:grob-property grob 'Y-extent)) - (note-heads (cn-note-heads-from-grob grob '())) + (note-heads (cn:note-heads-from-grob grob '())) (heads-edge (cn-grobs-edge note-heads up-stem)) (stem-tip (if up-stem (cdr stem-y-extent) (car stem-y-extent))) @@ -1300,7 +1226,7 @@ accidental-styles.none = #'(#t () ()) #(define (cn-customize-stem grob double-stems) ;; Lengthen all stems to undo staff compression side effects, ;; and give half notes double stems. - (let* ((bss-inverse (/ 1 (cn-get-base-staff-space grob))) + (let* ((bss-inverse (/ 1 (cn:get-base-staff-space grob))) (deets (ly:grob-property grob 'details)) (deets2 (cn-multiply-details deets bss-inverse '(stem-shorten)))) @@ -1368,7 +1294,7 @@ accidental-styles.none = #'(#t () ()) ;; START CLAIRNOTE EDITS ;; we just get the 1st notehead, does it matter which? - (note-head (list-ref (cn-note-heads-from-grob root '()) 0))) + (note-head (list-ref (cn:note-heads-from-grob root '()) 0))) ;; if is half note... (if (and note-head (= 1 (ly:grob-property note-head 'duration-log))) @@ -1437,7 +1363,7 @@ accidental-styles.none = #'(#t () ()) %--- DOTS ON DOTTED NOTES ---------------- #(define (cn-highest-semitone note-heads) - (reduce max -inf.0 (map cn-notehead-semitone note-heads))) + (reduce max -inf.0 (map cn:notehead-semitone note-heads))) #(define (cn-make-dots-callback is-rhythmic-staff) ;; Avoid collision between double-stem and dots by shifting right the dots @@ -1459,7 +1385,7 @@ accidental-styles.none = #'(#t () ()) (= 1 (ly:grob-property note-head 'duration-log)) ;; is line note? - (begin (set! semi (cn-notehead-semitone note-head)) + (begin (set! semi (cn:notehead-semitone note-head)) (or (= 0 (modulo semi 4)) is-rhythmic-staff)) ;; is up-stem? @@ -1469,7 +1395,7 @@ accidental-styles.none = #'(#t () ()) (= 1 (ly:grob-property stem 'direction)) ;; is highest note? - (let* ((note-heads (cn-note-heads-from-grob stem '()))) + (let* ((note-heads (cn:note-heads-from-grob stem '()))) (or (= 1 (length note-heads)) (= semi (cn-highest-semitone note-heads)))) @@ -1489,7 +1415,7 @@ accidental-styles.none = #'(#t () ()) #(define (cn-beam-grob-callback grob) ;; Adjust size and spacing of beams. ;; Needed due to vertically compressed staff. - (let* ((bss-inverse (/ 1 (cn-get-base-staff-space grob))) + (let* ((bss-inverse (/ 1 (cn:get-base-staff-space grob))) (thick (ly:grob-property grob 'beam-thickness)) (len-frac (ly:grob-property grob 'length-fraction)) (space (if (number? len-frac) len-frac 1))) @@ -1746,7 +1672,7 @@ accidental-styles.none = #'(#t () ()) #(define cnUnextendStaffDown #{ \cnStaffExtender ##f 0 -1 #}) #(define cnStaffOctaveSpan - (define-music-function (octaves) (positive-integer?) + (define-music-function (octaves) (cn:positive-integer?) ;; odd octaves: extended the same amount up and down (from 1) ;; even octaves: extended up one more than they are down (let* @@ -1903,8 +1829,8 @@ accidental-styles.none = #'(#t () ()) ;; Indicates number of octaves the staff spans, lets us use ;; different clef settings so stems always flip at center of staff. - (context-prop 'cnStaffOctaves positive-integer?) - (grob-prop 'cn-staff-octaves positive-integer?) + (context-prop 'cnStaffOctaves cn:positive-integer?) + (grob-prop 'cn-staff-octaves cn:positive-integer?) ;; For moving clef position up or down by one or more octaves. (context-prop 'cnClefShift integer?) @@ -1927,7 +1853,7 @@ accidental-styles.none = #'(#t () ()) ;; For Stem grobs, for double stems for half notes. (grob-prop 'cn-double-stem-spacing number?) - (grob-prop 'cn-double-stem-width-scale non-zero?) + (grob-prop 'cn-double-stem-width-scale cn:non-zero?) ;; Used to produce ledger line pattern. (grob-prop 'cn-ledger-recipe list?) @@ -1981,7 +1907,7 @@ clairnoteTypeUrl = "" clefTransposition = 0 middleCPosition = -12 - staffLineLayoutFunction = #cn-pitch-to-semitone + staffLineLayoutFunction = #cn:pitch-to-semitone printKeyCancellation = ##f \numericTimeSignature @@ -2072,7 +1998,7 @@ initClairnoteDN = #(define-scheme-function () () (set! cn-white-note? (lambda (grob) - (odd? (cn-notehead-semitone grob)))) + (odd? (cn:notehead-semitone grob)))) (set! cn-default-note-head-stencil-callback (cn-make-note-head-stencil-callback diff --git a/clairnote/init.scm b/clairnote/init.scm new file mode 100644 index 0000000..babc48b --- /dev/null +++ b/clairnote/init.scm @@ -0,0 +1,39 @@ +(use-modules + (srfi srfi-1) + (srfi srfi-13) + (lily)) + +;; for me (on guix) %compile-fallback-path is set to a non writable directory. +;; change it so that it is writable +;; TODO: other platforms +(unless (and (access? %compile-fallback-path W_OK) (file-is-directory? %compile-fallback-path)) + (set! %compile-fallback-path + (string-append (getenv "HOME") + "/.cache/guile/ccache/lilypond/"))) + +;; compiled code is faster and have debug info +(set! %load-should-auto-compile #t) +(ly:set-option 'compile-scheme-code #t) + +;; backtraces are nice +(debug-enable 'backtrace) + +(let ((guile-user-mod (resolve-module '(guile-user)))) + + (unless (module-variable guile-user-mod 'cn:clairnote-module-list) + (module-define! guile-user-mod + 'cn:clairnote-module-list + (map (lambda (x) + (resolve-interface x #:prefix 'cn:)) + '((clairnote utils))))) + + (map + (lambda (x) + ;; different from module-add! + ;; module-add! allows a var to be used by a module + ;; ly:module-copy makes vars available in anything that imports a module + ;; so this allows + ;; 1) cn:xyz be used in a scope such as \paper + ;; 2) lilypond a.ly b.ly to work with clairnote + (ly:module-copy (current-module) x)) + (module-ref guile-user-mod 'cn:clairnote-module-list))) diff --git a/clairnote/utils.scm b/clairnote/utils.scm new file mode 100644 index 0000000..a40c82b --- /dev/null +++ b/clairnote/utils.scm @@ -0,0 +1,86 @@ +(define-module (clairnote utils)) + +(use-modules + (srfi srfi-1) + (srfi srfi-13) + (lily)) + +(define-public (non-zero? n) (not (zero? n))) + +(define-public (positive-integer? n) (and (positive? n) (integer? n))) + +(define-public (map-pair proc pair) + (cons + (proc (car pair)) + (proc (cdr pair)))) + +(define-public (pitch-to-semitone pitch) + ;; Takes a pitch object and returns a semitone integer that corresponds to + ;; the pitch's position on the Clairnote staff. Used for + ;; staffLineLayoutFunction. The return value is almost always the semitone + ;; returned by (ly:pitch-semitones pitch) except for quarter tone + ;; alteration exceptions. 1/4 and 3/4 alterations are quarter tone sharps + ;; and their semitone needs to be adjusted down by one. + (let + ((alteration (ly:pitch-alteration pitch)) + (semitone (ly:pitch-semitones pitch))) + (cond + ((= 1/4 alteration) (- semitone 1)) + ((= 3/4 alteration) (- semitone 1)) + (else semitone)))) + +(define-public (notehead-pitch grob) + ;; Takes a note head grob and returns its pitch. + (define event (ly:grob-property grob 'cause)) + (if (ly:stream-event? event) + (ly:event-property event 'pitch) + (begin + (ly:warning "clairnote.ly cannot access the pitch of a note head grob. (Are you trying to use the Ambitus_engraver? It is incompatible with clairnote.ly.)") + (ly:make-pitch 0 0 0)))) + +(define-public (notehead-semitone grob) + ;; Takes a note head grob and returns its semitone. + (pitch-to-semitone (notehead-pitch grob))) + +(define-public (staff-symbol-property grob prop default) + ;; Takes a grob @var{grob}, a symbol @var{prop}, and + ;; a @var{default} value. Returns that custom StaffSymbol + ;; property or silently falls back to the default value. + (define staff-sym (ly:grob-object grob 'staff-symbol)) + (if (ly:grob? staff-sym) + (ly:grob-property staff-sym prop) + default)) + +(define-public (get-base-staff-space grob) + ;; Takes a grob and returns the custom StaffSymbol property + ;; cn-base-staff-space. Silently falls back to the default of 0.75. + (staff-symbol-property grob 'cn-base-staff-space 0.75)) + +(define-public (magnification grob) + ;; Return the current magnification (from magnifyStaff, etc.) + ;; via a grob's font size. + (magstep (ly:grob-property grob 'font-size 0))) + +(define-public (get-staff-clef-adjust staff-octaves clef-octave-shift) + ;; Calculate the amount to vertically adjust the position of the clef, + ;; key signature, and time signature, in note-spaces / half-staff-spaces. + (+ + (* 12 clef-octave-shift) + (if (odd? staff-octaves) + 6 + (if (> staff-octaves 2) 12 0)))) + +(define-public (staff-clef-adjust-from-grob grob) + (get-staff-clef-adjust + (staff-symbol-property grob 'cn-staff-octaves 2) + (staff-symbol-property grob 'cn-clef-shift 0))) + +(define-public (note-heads-from-grob grob default) + ;; Takes a grob like a Stem and returns a list of + ;; NoteHead grobs or default. + (let* ((heads-array (ly:grob-object grob 'note-heads)) + (heads-list (if (ly:grob-array? heads-array) + (ly:grob-array->list heads-array) + ;; should never/rarely? happen: + default))) + heads-list)) diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..1bd5a1f --- /dev/null +++ b/flake.lock @@ -0,0 +1,96 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "id": "flake-utils", + "type": "indirect" + } + }, + "lilypond-2-24": { + "flake": false, + "locked": { + "lastModified": 1721474473, + "narHash": "sha256-CvbVQUXE0rbU8599YA76CB8xmf1cxwwIm8ZywyLKHfY=", + "owner": "lilypond", + "repo": "lilypond", + "rev": "b7908403efe0761cec86c94db0c529db031246a0", + "type": "github" + }, + "original": { + "owner": "lilypond", + "ref": "v2.24.4", + "repo": "lilypond", + "type": "github" + } + }, + "lilypond-2-25": { + "flake": false, + "locked": { + "lastModified": 1720954141, + "narHash": "sha256-haiVEMeeIggUQjGJ6i0zj20JEn5VW1tPOrTlwDsmQaQ=", + "owner": "lilypond", + "repo": "lilypond", + "rev": "f1777509159e606513829c6794ec57782b7cacd9", + "type": "github" + }, + "original": { + "owner": "lilypond", + "ref": "v2.25.18", + "repo": "lilypond", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1722141560, + "narHash": "sha256-Ul3rIdesWaiW56PS/Ak3UlJdkwBrD4UcagCmXZR9Z7Y=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "038fb464fcfa79b4f08131b07f2d8c9a6bcc4160", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "lilypond-2-24": "lilypond-2-24", + "lilypond-2-25": "lilypond-2-25", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..18f20a5 --- /dev/null +++ b/flake.nix @@ -0,0 +1,37 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + flake-utils.url = "flake-utils"; + + lilypond-2-24 = { + url = "github:lilypond/lilypond/v2.24.4"; + flake = false; + }; + lilypond-2-25 = { + url = "github:lilypond/lilypond/v2.25.18"; + flake = false; + }; + }; + + outputs = { + self, + nixpkgs, + flake-utils, + lilypond-2-24, + lilypond-2-25, + }: + flake-utils.lib.eachDefaultSystem ( + system: let + pkgs = nixpkgs.legacyPackages.${system}; + in { + packages.lilypond-2-24 = pkgs.lilypond.overrideAttrs { + version = "2.24.4"; + src = lilypond-2-24; + }; + packages.lilypond-2-25 = pkgs.lilypond.overrideAttrs { + version = "2.25.18"; + src = lilypond-2-25; + }; + } + ); +} diff --git a/scripts/test.py b/scripts/test.py new file mode 100755 index 0000000..91ebd65 --- /dev/null +++ b/scripts/test.py @@ -0,0 +1,214 @@ +#!/bin/env python3 + +import argparse +import asyncio +import multiprocessing +import os +import shutil +import tempfile +from asyncio.subprocess import Process +from pathlib import Path + + +# https://stackoverflow.com/questions/287871/how-do-i-print-colored-text-to-the-terminal +class bcolors: + HEADER = "\033[95m" + OKBLUE = "\033[94m" + OKCYAN = "\033[96m" + OKGREEN = "\033[92m" + WARNING = "\033[93m" + FAIL = "\033[91m" + ENDC = "\033[0m" + BOLD = "\033[1m" + UNDERLINE = "\033[4m" + + +output_func_queue = asyncio.Queue() +fails = [] + + +async def run_queue_in_seq(): + while True: + item = await output_func_queue.get() + if item is None: + return + await item() + + +async def run_one_test(file: Path, outpath: Path, process: Process): + + async def show_stdout(): + stdout = process.stdout + assert stdout is not None + aggr = [] + async for line in stdout: + aggr.append(line) + print(f"{file.name}>> {line.decode(errors='replace')}", end="") + return b"".join(aggr) + + async def show_stderr(): + stderr = process.stderr + assert stderr is not None + aggr = [] + async for line in stderr: + aggr.append(line) + print(f"{file.name}> {line.decode(errors='replace')}", end="") + return b"".join(aggr) + + msgs_list = [] + + async def wait_one(): + code, out, err = await asyncio.gather( + process.wait(), show_stdout(), show_stderr() + ) + print() + for x in msgs_list: + print(x) + if not code == 0: + fails.append(str(file)) + print(f"{bcolors.FAIL}{file.name}: Failed with code {code}{bcolors.ENDC}") + if out: + outpath.with_suffix(".stdout").write_bytes(out) + if err: + outpath.with_suffix(".stderr").write_bytes(err) + print() + + await output_func_queue.put(wait_one) + try: + await asyncio.wait_for(process.wait(), timeout=30) + except asyncio.TimeoutError: + msgs_list.append( + f"{bcolors.FAIL}{file.name}: terminating due to timeout{bcolors.ENDC}" + ) + process.terminate() + await asyncio.sleep(1) + if process.returncode is None: + msgs_list.append(f"{bcolors.FAIL}{file.name}: killing{bcolors.ENDC}") + process.kill() + + +async def main(): + parser = argparse.ArgumentParser() + + parser.add_argument("files", nargs="*") + parser.add_argument("-l", "--lilypond", type=str, default=shutil.which("lilypond")) + parser.add_argument("-o", "--out", type=str, required=True) + parser.add_argument("-i", "--include", type=str, action="append", default=[]) + parser.add_argument("--cpu", type=int, default=multiprocessing.cpu_count()) + + args = parser.parse_args() + + lily_bin = Path(args.lilypond).absolute() + assert lily_bin.exists() + print(f"using {lily_bin}") + + if args.files: + args_files = [Path(x).resolve() for x in sorted(args.files)] + else: + args_files = [Path(__file__).parent.parent / "test" / "tests"] + + seen = set() + + def walk(p: Path): + p = p.resolve() + if p not in seen: + seen.add(p) + if p.is_dir(): + for c in p.iterdir(): + yield from walk(c) + elif p.suffix == ".ly": + yield p + + files: list[tuple[Path, Path]] = [] + out_subdirs = set() + for file in args_files: + for x in walk(file): + rel = x.relative_to(file) + assert not ".." in rel.parts + if rel in out_subdirs: + raise RuntimeError(f"conflicting output: {rel}") + out_subdirs.add(rel) + files.append((x, rel)) + + files = sorted(files) + + print(f"running {len(files)} tests") + + ly_args = [ + "-I", + str(Path(__file__).parent), + ] + includes = args.include + + if len(includes) > 1: + raise NotImplementedError() + + for x in includes: + ly_args.append("-d") + ly_args.append(f"include-settings={x}") + + sem = asyncio.Semaphore(args.cpu) + + outpath = Path(args.out).resolve() + print(f"outputing to {outpath}") + if not outpath.exists(): + outpath.mkdir(parents=True) + + assert outpath.is_dir() + + with tempfile.TemporaryDirectory() as tmp_dir: + + print(f"using tmp home: {tmp_dir}") + + async def test_one(file: Path, outrel: Path): + async with sem: + outrel = outpath / outrel.with_suffix("") + outrel.parent.mkdir(parents=True, exist_ok=True) + proc = await asyncio.create_subprocess_exec( + lily_bin, + "--output", + str(outrel), + *ly_args, + str(file), + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd="/", + env={ + # for fontconfig and guile cache + "HOME": tmp_dir, + "CLAIRNOTE": os.environ.get("CLAIRNOTE") or "default", + }, + ) + await run_one_test(file, outrel, proc) + + jobs = [] + for file, outrel in files: + jobs.append(test_one(file, outrel)) + + async def run_jobs(): + await asyncio.gather(*jobs) + await output_func_queue.put(None) + + try: + await asyncio.gather(run_queue_in_seq(), run_jobs()) + except KeyboardInterrupt: + print("[KeyboardInterrupt]") + finally: + if fails: + print() + print(f"{len(fails)} failed:") + for f in fails: + print(f) + exit(1) + + +loop = asyncio.new_event_loop() +asyncio.set_event_loop(loop) +try: + loop.run_until_complete(main()) +except KeyboardInterrupt: + tasks = [t for t in asyncio.all_tasks(loop) if not t.done()] + [task.cancel() for task in tasks] + loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True)) +finally: + loop.close() diff --git a/test/test-includes.ly b/test/test-includes.ly index 2b80ce8..4bd1d8d 100644 --- a/test/test-includes.ly +++ b/test/test-includes.ly @@ -1,11 +1,16 @@ \version "2.19.63" -% clairnote-type = dn +#(define clairnote-type (or (getenv "CLAIRNOTE") "default")) + +% supress the version error +#(define version-seen #t) + +#(debug-set! width 200) \include "../clairnote.ly" #(set-default-paper-size "a4landscape") - + \paper { indent = 0 } diff --git a/test/tests/rests-in-voices.ly b/test/tests/rests-in-voices.ly index dbddd2a..4ca4b2e 100644 --- a/test/tests/rests-in-voices.ly +++ b/test/tests/rests-in-voices.ly @@ -1,6 +1,6 @@ \version "2.22.2" % clairnote-type = "dn" -\include "clairnote.ly" +\include "../test-includes.ly" \language "english" #(set-default-paper-size "letter")