-
Notifications
You must be signed in to change notification settings - Fork 17
/
eldev-build.el
877 lines (814 loc) · 52.2 KB
/
eldev-build.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
;;; eldev-build.el --- Elisp development tool -*- lexical-binding: t -*-
;;; Copyright (C) 2019-2024 Paul Pogonyshev
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation, either version 3 of
;; the License, or (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see https://www.gnu.org/licenses.
(require 'eldev)
;; To silence byte-compilation warnings on Emacs 24-25.
(defvar byte-compile-log-warning-function)
(defvar eldev--build-targets (make-hash-table :test #'equal))
(defvar eldev--targets-prepared-for nil)
(defvar eldev--build-plan nil)
(defvar eldev--build-results nil)
(defun eldev-build-find-targets (&rest standard-filesets)
"Return a hash table of all build targets in given filesets.
STANDARD-FILESETS must be a list of names from `eldev-filesets'
variable or `all'. The hash table might contain targets from
other filesets too. Caller must not modify contents of the
returned hash table.
Hash table keys are strings, i.e. names of targets. Entries,
each describing a builder invocations, are alists with at least
the following keys, in no particular order:
- builder: name (symbol) of the builder that can generate this
target, see macro `eldev-defbuilder';
- sources: list of files or other targets that serve as sources
for this one; each entry must be a file or a real target, not
virtual;
- targets: all targets that will be build by this invocation;
will include at least the target used as the key, but may
include more (depends on the builder).
Returned hash table doesn't contain cross-target dependency
information past the list of sources. For this, use function
`eldev-get-target-dependencies'."
(setf standard-filesets (eldev--effective-standard-filesets standard-filesets))
(when (memq 'test standard-filesets)
(eldev--inject-loading-roots 'test))
(let ((new-filesets (eldev-filter (not (memq it eldev--targets-prepared-for)) standard-filesets)))
(when new-filesets
(setf new-filesets (nreverse new-filesets))
(eldev-trace "Generating build target list for %s" (eldev-message-enumerate "standard fileset" new-filesets))
;; FIXME: The point of special-casing `package' builder is to let it know all the
;; sources at once, because its target depends on the number of sources
;; (`.el' if one file, `.tar' otherwise). This is of course a hack, but it
;; is not clear how to generalize that. Postponed until some real-world
;; needs come up.
(let* ((special-cased-builder (assq 'package eldev--builders))
(reordered-builders (if special-cased-builder
(append (remq special-cased-builder eldev--builders) (list special-cased-builder))
eldev--builders))
(potential-new-sources (eldev-find-files (apply #'eldev-standard-filesets new-filesets)))
(visited-sources (make-hash-table :test #'equal))
special-cased-builder-sources
new-sources)
(while (setf new-sources (eldev-filter-files (eldev-filter (not (gethash it visited-sources)) potential-new-sources)
`(:not ,eldev-standard-excludes)))
(dolist (source new-sources)
(puthash source t visited-sources))
(setf potential-new-sources nil)
(dolist (entry reordered-builders)
(let* ((builder-name (car entry))
(builder (cdr entry))
(source-files (eldev-get builder :source-files)))
(when source-files
(setf source-files (eldev-filter-files new-sources source-files)))
(when (eq entry special-cased-builder)
(setf source-files (append special-cased-builder-sources source-files)))
(when source-files
(if (eq entry special-cased-builder)
(setf special-cased-builder-sources source-files)
(dolist (invocation (eldev--build-find-builder-invocations source-files builder builder-name))
(let ((sources (car invocation))
(targets (cdr invocation)))
(eldev--build-target-entries targets builder builder-name sources)
(setf potential-new-sources (append targets potential-new-sources)))))))))
(when special-cased-builder-sources
(let ((builder-name (car special-cased-builder))
(builder (cdr special-cased-builder)))
(dolist (invocation (eldev--build-find-builder-invocations special-cased-builder-sources builder builder-name))
(eldev--build-target-entries (cdr invocation) builder builder-name (car invocation))))))))
(unless (gethash ":default" eldev--build-targets)
(eldev--build-target-entries '(":default") nil nil nil))
eldev--build-targets)
(defun eldev--effective-standard-filesets (standard-filesets)
(if (or (null standard-filesets) (memq 'all standard-filesets))
(mapcar #'car eldev-filesets)
standard-filesets))
(defun eldev--build-find-builder-invocations (sources builder builder-name)
(let ((target-rule (eldev-get builder :targets))
invocations
ignored-targets)
(when (functionp target-rule)
(setf target-rule (funcall target-rule sources)))
(dolist (invocation (pcase target-rule
((pred stringp)
`((,sources . (,target-rule))))
((pred eldev-string-list-p)
`((,sources . ,target-rule)))
(`(,(and (or (pred stringp) (pred eldev-string-list-p)) source-suffixes) -> ,(and (pred stringp) target-suffix))
(setf source-suffixes (eldev-listify source-suffixes))
(let (result)
(dolist (source sources)
(let ((scan source-suffixes)
found)
(while scan
(let ((source-suffix (pop scan)))
(when (string-suffix-p source-suffix source)
(push `((,source) . (,(eldev-replace-suffix source source-suffix target-suffix))) result)
(setf scan nil
found t))))
(unless found
(error "Builder `%s': name of source file `%s' doesn't end with %s as `:targets' wants"
builder-name source (eldev-message-enumerate nil source-suffixes nil nil "or")))))
(nreverse result)))
(_ (error "Invalid `:targets' (or its return value) %S in builder `%s'" target-rule builder-name))))
;; Discard builder's wishes that match `eldev-build-ignored-target-fileset'.
(let* ((targets (cdr invocation))
(ignored (when eldev-build-ignored-target-fileset
(eldev-filter-files targets eldev-build-ignored-target-fileset))))
(when ignored
(setf ignored-targets (append ignored-targets ignored)))
(when (> (length targets) (length ignored))
(push `(,(car invocation) . ,(eldev-filter (not (member it ignored)) targets)) invocations))))
(when ignored-targets
(eldev-trace "%s" (eldev-message-enumerate-files "Ignored potential target%s: %s (%d)" ignored-targets)))
(nreverse invocations)))
(defun eldev--build-target-entries (targets builder builder-name sources)
(let ((sorted-targets (sort (copy-sequence targets) #'string<))
shared-entry
created-new-entry)
(dolist (target targets)
(if (eldev-virtual-target-p target)
(when builder-name
(error "Virtual targets (e.g. `%s') must not have builders" target))
(unless builder-name
(error "Real targets (e.g. `%s') must have associated builders" target)))
(let ((entry (gethash target eldev--build-targets)))
(if shared-entry
(if (or entry (not created-new-entry))
(unless (eq entry shared-entry)
(error "Unexpected entry %S for target `%s': expected %S" entry target shared-entry))
(setf entry shared-entry))
(if entry
(let ((entry-builder (cdr (assq 'builder entry))))
(unless (eq entry-builder builder-name)
(error "Conflicting builders `%s' and `%s' for target `%s'" entry-builder builder-name target))
;; Since both target lists are sorted, we can compare just like this.
(unless (equal (cdr (assq 'sorted-targets entry)) sorted-targets)
(error "Unexpected full target list %s for target `%s': expected %s"
(eldev-message-enumerate nil (cdr (assq 'targets entry)) nil nil t) target (eldev-message-enumerate nil targets nil nil t))))
(setf entry `((builder . ,builder-name)
(targets . ,targets)
(sorted-targets . ,sorted-targets))
created-new-entry t))
(let* ((existing-sources (cdr (assq 'sources entry)))
(new-sources (if existing-sources (eldev-filter (not (member it existing-sources)) sources) sources)))
(eldev--assq-set 'sources (append existing-sources new-sources) entry))
(setf shared-entry entry))
(puthash target entry eldev--build-targets))))
(when builder
(eldev--build-collect-targets targets builder)))
(defun eldev--build-collect-targets (targets builder)
(let ((collect (eldev-get builder :collect)))
(when (functionp collect)
(setf collect (funcall collect targets)))
(dolist (entry (cond ((or (stringp collect) (eldev-string-list-p collect))
`((,targets ,collect)))
((eldev-all-p (pcase it (`(,(or (pred stringp) (pred eldev-string-list-p))
,(or (pred stringp) (pred eldev-string-list-p)))
t))
collect)
collect)))
(let ((entry-targets (eldev-listify (car entry))))
(dolist (virtual-target (reverse (eldev-listify (cadr entry))))
(unless (eldev-virtual-target-p virtual-target)
(error "Expected a virtual target, `%s' isn't" virtual-target))
(eldev--build-target-entries (list virtual-target) nil nil entry-targets)
(setf entry-targets (list virtual-target)))))))
(defun eldev-get-target-dependencies (target &optional finder)
"Get TARGET's dependencies.
Usually, this function should be of no interest: custom builders
should normally use only `eldev-set-target-dependencies'.
However, it is allowed to use this function as described below.
By default, list of all dependencies, i.e. across all possible
dependency finders, is returned. However, if argument FINDER is
specified, returned list includes dependencies only for that
finder. In any case, the returned list is in no particular
order. Pass it through `eldev-canonical-dependency-order' if
needed.
Returned list's elements look like (TYPE DEPENDENCY [...]).
Following types (symbols) are currently defined:
- depends-on: DEPENDENCY must always be built (or be
up-to-date) before TARGET is built;
- inherits: TARGET inherits all sources (which become
`depends-on' dependencies) and dependencies of DEPENDENCY;
this is less strict than `depends-on';
For future compatibility, callers should treat any unknown type
as `depends-on'. It must not assume anything about contents of
the elements past the two described values.
Returned list must not be modified. Instead, use function
`eldev-set-target-dependencies' when needed.
This function may only be called while inside the body of a
`eldev-with-target-dependencies' macro."
;; This function is for public use, so don't "notice" dependency information if it is
;; internal.
(unless (car eldev--target-dependencies)
(error "May only be called inside `eldev-with-target-dependencies' macro"))
(eldev--do-get-target-dependencies target finder))
(defun eldev--do-get-target-dependencies (target &optional finder)
(let ((dependencies (gethash target (cdr eldev--target-dependencies))))
(if finder
(cdr (assq finder dependencies))
(let (all-dependencies)
(dolist (entry dependencies)
(dolist (dependency (cdr entry))
;; FIXME: O(N*N), but probably doesn't matter, as we don't
;; expect large lists here.
(unless (member dependency all-dependencies)
(push dependency all-dependencies))))
all-dependencies))))
(defun eldev-canonical-dependency-order (dependencies)
"Sort DEPENDENCIES into a canonical order.
The list is not sorted in-place, instead a sorted copy is
returned. Particular way of sorting is not guaranteed (i.e. it
may change in future), but will stay the same within one Eldev
invocation.
Since 1.7."
(sort (copy-sequence dependencies) #'eldev-dependency-entry-lessp))
(defun eldev-dependency-entry-lessp (a b)
"Compare dependency entries A and B.
You probably shouldn't use this directly, instead call
`eldev-canonical-dependency-order'.
See `eldev-get-target-dependencies' for a description of entry
format. This function can be used to sort a list of such entries
into a “canonical” order. How dependencies are compared is not
specified (i.e. it may change in future), but is guaranteed to
stay the same within one Eldev invocation.
Since 1.7."
(if (eq (car a) (car b))
(string< (cadr a) (cadr b))
(string< (symbol-name (car a)) (symbol-name (car b)))))
(defun eldev-set-target-dependencies (target finder dependencies)
"Set the list of TARGET's DEPENDENCIES according to given FINDER.
FINDER should be a unique symbol, e.g. caller function name. The
purpose of it is that when function is called for the same target
and finder again, it replaces previous dependencies, but only
those found by the same finder. This way several finders can
cooperate to find exhaustive dependency list without even knowing
of each other.
See documentation of `eldev-get-target-dependencies' for list's
elements description.
Evaluates to non-nil if FINDER's dependencies are changed, to nil
if they are exactly the same as before (possibly in different
order). In some cases, even if return value is non-nil, final
dependencies can remain the same because of different finders.
This function may only be called while inside the body of a
`eldev-with-target-dependencies' macro."
(when (or eldev--target-dependencies-need-saving ; Skip sorting and comparing if dependencies are already modified.
(let ((current-dependencies (eldev-get-target-dependencies target finder)))
(not (equal (eldev-canonical-dependency-order dependencies) (eldev-canonical-dependency-order current-dependencies)))))
(eldev--assq-set finder (copy-sequence dependencies) (gethash target (cdr eldev--target-dependencies)) #'equal)
(setf eldev--target-dependencies-need-saving t)))
(defun eldev--do-targets (parameters)
(let ((all-targets (if parameters
(apply #'eldev-build-find-targets (mapcar #'eldev-validate-standard-fileset parameters))
(eldev-build-find-targets 'main))))
(if (> (hash-table-count all-targets) 0)
(let ((sources (make-hash-table :test #'equal))
toplevel)
(maphash (lambda (_target entry)
(dolist (source (cdr (assq 'sources entry)))
(puthash source t sources)))
all-targets)
(maphash (lambda (target _)
(unless (gethash target sources)
(push target toplevel)))
all-targets)
(eldev-with-target-dependencies
;; This is done only to detect cyclic dependencies.
(let ((eldev--build-plan (make-hash-table :test #'equal)))
(maphash (lambda (target _entry) (eldev--build-add-to-plan target all-targets)) all-targets))
(eldev--print-target-level toplevel 0 all-targets (make-hash-table :test #'eq))))
(eldev-print "There are no targets for %s" (if (> (length parameters) 1) "these filesets" "this fileset")))))
(defun eldev--print-target-level (level-targets level all-targets printed-target-entries)
(when level-targets
(let ((indentation (make-string (* level 4) ? ))
(level-targets (copy-sequence level-targets))
level-sources)
(setf level-targets (sort level-targets #'string<))
(dolist (prioritized-target '(":package" ":default"))
(when (member prioritized-target level-targets)
(setf level-targets `(,prioritized-target ,@(delete prioritized-target level-targets)))))
(dolist (target level-targets)
(let* ((entry (gethash target all-targets))
(repeated (gethash entry printed-target-entries))
(builder (cdr (assq 'builder entry)))
(sources (cdr (assq 'sources entry)))
(target-name (eldev-colorize target (cond ((eldev-virtual-target-p target) 'section) (sources 'name)) target)))
(cond ((and (null sources) (eq eldev-targets-list-sources 'concise))
(push target level-sources))
((or sources eldev-targets-list-sources (eldev-virtual-target-p target))
(cond ((and repeated builder)
(eldev-output "%s%s [%s]" indentation target-name
(eldev-colorize (if (equal repeated target) "repeated, see above" (eldev-format-message "repeated, see `%s' above" repeated)) 'details)))
((and builder (eldev-unless-quiet t))
(eldev-output "%s%s [%s]" indentation target-name (or (eldev-get (cdr (assq builder eldev--builders)) :short-name) builder)))
(t
(eldev-output "%s%s" indentation target-name)))
(unless repeated
(puthash entry target printed-target-entries)
(eldev--print-target-level sources (1+ level) all-targets printed-target-entries)
(when eldev-targets-list-dependencies
(dolist (dependency (eldev-canonical-dependency-order (eldev-get-target-dependencies target)))
(eldev-output "%s %s %s" indentation
(eldev-colorize (eldev-pcase-exhaustive (car dependency)
(`depends-on "[dep]")
(`inherits "[inh]"))
'details)
(cadr dependency)))))))))
(when level-sources
(setf level-sources (nreverse level-sources))
(let ((num (length level-sources)))
(when (> num 3)
(setf (nthcdr 3 level-sources) nil))
(eldev-output "%s%s%s" indentation (mapconcat #'identity level-sources (eldev-colorize ", " 'details))
(if (> num 3) (eldev-colorize (eldev-format-message " + %d more" (- num 3)) 'details) "")))))))
(defun eldev--do-build (parameters &optional dont-touch-packages)
;; When building, project loading mode is ignored. The reason is that building itself
;; can involve compiling or packaging.
(run-hooks 'eldev-build-system-hook)
(unless eldev-normal-dependency-management
(setf dont-touch-packages t))
(unless dont-touch-packages
(let ((eldev-project-loading-mode 'as-is)
(extra-dependencies '(build)))
(when (memq 'test (eldev--effective-standard-filesets eldev-build-sets))
(push 'test extra-dependencies)
;; FIXME: Is root injection actually needed here? It will also be done from the
;; call to `eldev-build-find-targets' just a few lines below.
(eldev--inject-loading-roots 'test))
(eldev-load-project-dependencies extra-dependencies nil t)))
(let ((all-targets (apply #'eldev-build-find-targets (or eldev-build-sets '(main))))
target-list
target-fileset
virtual-targets)
(if parameters
(dolist (parameter parameters)
(push parameter (if (eldev-virtual-target-p parameter) virtual-targets target-fileset)))
(setf virtual-targets '(":default")))
(dolist (target (setf virtual-targets (nreverse virtual-targets)))
(unless (gethash target all-targets)
(signal 'eldev-error `("Unknown virtual target `%s'" ,target))))
(when target-fileset
(maphash (lambda (target _entry) (push target target-list)) all-targets)
(setf target-list (eldev-filter-files target-list (nreverse target-fileset))))
(eldev-with-target-dependencies
(let ((eldev--build-plan (make-hash-table :test #'equal))
(eldev--build-results (make-hash-table :test #'equal))
build-sequence
anything-failed)
;; We create a plan before even starting to build everything, so that cyclic
;; dependencies can be detected earlier.
(dolist (target (nconc virtual-targets target-list))
(eldev--build-add-to-plan target all-targets))
(maphash (lambda (target order) (when (numberp order) (push (cons target order) build-sequence))) eldev--build-plan)
;; FIXME: Really could add nicety by sorting this more intelligently so that
;; e.g. targets go alphabetically and the same builder is used sequentially
;; where this doesn't break dependency ordering.
(when build-sequence
(setf build-sequence (sort build-sequence (lambda (a b) (< (cdr a) (cdr b)))))
(eldev-trace "Building plan: %s" (eldev-message-enumerate nil build-sequence #'car nil t))
(eldev-autoinstalling-implicit-dependencies t
(dolist (entry build-sequence)
(if eldev-build-keep-going
;; Ignore errors here: they will have been reported in `eldev-build-target'
;; already.
(condition-case nil
(eldev-build-target (car entry))
(eldev-build-abort-branch))
(eldev-build-target (car entry))))))
(when (= (hash-table-count eldev--build-results) 0)
(eldev-print-nothing-to-do))
(maphash (lambda (_target status) (unless (eq status 'built) (setf anything-failed t))) eldev--build-results)
;; See e.g. test `eldev-loading-modes-3'. Basically, once we load the main
;; project's package (in `as-is' mode), it would never get reloaded later, which
;; would be problematic if this build process was not the main invocation,
;; e.g. was only invoked from `autoloads' plugin. For now, simply always "unload"
;; the main package: in the main invocation simply nothing would happen afterwards
;; anyway. The only exception is when compiling on-demand.
(unless dont-touch-packages
(eldev--unload-package (package-desc-name (eldev-package-descriptor))))
(when anything-failed
(signal 'eldev-error `("Build failed")))))))
(defun eldev--need-to-build (target source)
"Determine if we need to build a non-virtual TARGET because of SOURCE."
(unless (eldev-virtual-target-p source)
(or (eq eldev-build-infinitely-new t)
(member source eldev-build-infinitely-new)
(file-newer-than-file-p source target))))
(defun eldev--need-to-build-full (target dependency all-targets &optional dependency-stack)
(let ((cycle (member dependency dependency-stack)))
(when cycle
(signal 'eldev-error `("%s form a dependency cycle" ,(eldev-message-enumerate "Target" (reverse dependency-stack))))))
;; `on-demand' is a special case, search for uses. In this case we don't have a target
;; map or dependency map prepared.
(let* ((on-demand (eq all-targets 'on-demand))
(entry (unless on-demand (gethash dependency all-targets))))
(when (or entry on-demand)
(push dependency dependency-stack)
(or (eldev-any-p (eldev--need-to-build target it) (if on-demand (list (eldev-replace-suffix dependency ".elc" ".el")) (cdr (assq 'sources entry))))
(eldev-any-p (eldev-pcase-exhaustive (car it)
(`depends-on (eldev--need-to-build target (cadr it)))
(`inherits (eldev--need-to-build-full target (cadr it) all-targets dependency-stack)))
(if on-demand
(progn (eldev--load-target-dependencies nil t)
(eldev--do-get-target-dependencies dependency 'eldev-builder-byte-compile-.el))
(eldev-get-target-dependencies dependency)))))))
(defun eldev--build-add-to-plan (target all-targets &optional dependency-stack)
(let ((already-planned (gethash target eldev--build-plan)))
(when (eq already-planned 'side-effect)
(puthash target 'planned-side-effect eldev--build-plan))
(or already-planned
(let ((cycle (member target dependency-stack)))
(when cycle
(signal 'eldev-error `("%s form a dependency cycle" ,(eldev-message-enumerate "Target" (reverse dependency-stack))))))
(let ((entry (gethash target all-targets))
(virtual (eldev-virtual-target-p target))
(need-to-build (or (eq eldev-build-force-rebuilding t)
(member target eldev-build-force-rebuilding))))
(when entry
(push target dependency-stack)
(dolist (source (cdr (assq 'sources entry)))
(when (or (eldev--build-add-to-plan source all-targets dependency-stack)
(unless virtual
(eldev--need-to-build target source)))
(setf need-to-build t)))
(dolist (dependency-entry (eldev-get-target-dependencies target))
(let ((dependency (cadr dependency-entry)))
(eldev-pcase-exhaustive (car dependency-entry)
(`depends-on (when (or (eldev--build-add-to-plan dependency all-targets dependency-stack)
(unless virtual
(eldev--need-to-build target dependency)))
(setf need-to-build t)))
(`inherits (when (eldev--need-to-build-full target dependency all-targets dependency-stack)
(setf need-to-build t))))))
;; For the main target it will be overwritten.
(dolist (other-target (cdr (assq 'targets entry)))
(puthash other-target 'side-effect eldev--build-plan))
(puthash target (when need-to-build (hash-table-count eldev--build-plan)) eldev--build-plan))))))
(defun eldev-build-target-status (target)
"Returns TARGET building result.
Return value is one of the following symbols:
- planned
- building
- built
- failed
- not-planned"
(or (gethash target eldev--build-results)
;; Both numbers and symbol `planned-side-effect' mean that the target is planned for
;; building.
(if (memq (gethash target eldev--build-plan) '(nil side-effect)) 'not-planned 'planned)))
(defun eldev-build-target (target)
"Build given TARGET.
The TARGET must be previously planned for building: it is not
possible to build arbitrary targets this way."
(eldev-pcase-exhaustive (eldev-build-target-status target)
(`building (error "Trying to build `%s' recursively" target))
(`built (eldev-trace "Not trying to build `%s' again" target))
(`failed (error "Building `%s' failed earlier" target))
(`not-planned (error "Building `%s' was never planned" target))
(`planned
;; Reset `default-directory', because it can have been changed e.g. when called from
;; inside byte-compilation.
(let* ((default-directory eldev-project-dir)
(entry (gethash target eldev--build-targets))
(builder-name (cdr (assq 'builder entry)))
(targets (cdr (assq 'targets entry)))
succeeded)
(puthash target 'building eldev--build-results)
(unwind-protect
(if builder-name
(let* ((builder (cdr (assq builder-name eldev--builders)))
(builder-type (or (eldev-get builder :type) 'one-to-one))
(sources (cdr (assq 'sources entry)))
;; Normally such things will not be in the plan to begin with, but
;; also take into account situations where builders don't update
;; their target for whatever reason, e.g. when they can detect that
;; it hasn't changed.
(need-to-build (or (eq eldev-build-force-rebuilding t)
(member target eldev-build-force-rebuilding)
(eldev--need-to-build-full target target eldev--build-targets))))
(if need-to-build
(let* ((eldev-build-current-targets targets)
(source-argument (if (memq builder-type '(one-to-one one-to-many)) (car sources) sources))
(target-argument (if (memq builder-type '(one-to-one many-to-one)) (car targets) targets)))
(eldev-unless-quiet
(let* ((short-name (or (eldev-get builder :short-name) builder-name))
(message (or (eldev-get builder :message) 'sources))
(source-string (if (cddr sources)
(if (eldev-when-verbose t)
(eldev-message-enumerate nil sources (lambda (source) (eldev-colorize source 'name)) t)
(eldev-format-message "%d files" (length sources)))
(eldev-colorize (car sources) 'name)))
(target-string (if (cddr targets)
(if (eldev-when-verbose t)
(eldev-message-enumerate nil targets (lambda (target) (eldev-colorize target 'name)) t)
(eldev-format-message "%d files" (length targets)))
(eldev-colorize (car targets) 'name))))
(pcase message
((or `source `sources)
(eldev-output "%-8s %s" short-name source-string))
((or `target `targets)
(eldev-output "%-8s -> %s" short-name target-string))
((or `source-and-target `sources-and-target `source-and-targets `sources-and-targets)
(eldev-output "%-8s %s -> %s" short-name source-string target-string))
(_
(eldev-output "%-8s %s" short-name (funcall message sources targets))))))
(unless eldev-build-dry-run-mode
(if eldev-build-keep-going
(condition-case error
(eldev-backtrace-notch 'eldev
(if (eldev-get builder :profiling-self)
(funcall builder source-argument target-argument)
(eldev-profile-body
(funcall builder source-argument target-argument))))
(eldev-build-abort-branch
(signal (car error) (cdr error)))
(eldev-error
(eldev-error "While building `%s': %s" target (apply #'eldev-format-message (cdr error)))
(signal 'eldev-build-abort-branch nil))
(error
(eldev-error "While building `%s': %s" target (error-message-string error))
(signal 'eldev-build-abort-branch nil)))
(funcall builder source-argument target-argument)))
(setf succeeded t))
(setf succeeded t)
(eldev-verbose "Not building target `%s': it is up-to-date" target)))
(setf succeeded t)
(eldev-verbose "Done building “sources” for virtual target `%s'" target))
(dolist (target targets)
(puthash target (if succeeded 'built 'failed) eldev--build-results)))))))
(defconst eldev--have-byte-compile-warning-function (boundp 'byte-compile-log-warning-function))
(defvar eldev--recursive-byte-compilation nil)
(defvar eldev--recursive-elevated-errors-in nil)
(defun eldev--byte-compile-.el (source target)
(eval-and-compile (require 'bytecomp))
(let* ((recursive eldev--recursive-byte-compilation)
(eldev--recursive-byte-compilation t)
(eldev--recursive-elevated-errors-in (if recursive eldev--recursive-elevated-errors-in (list nil)))
(load-prefer-newer t)
;; When called recursively, let `byte-compile-file' determine this.
(skip-byte-compilation (unless recursive
(with-temp-buffer
(insert-file-contents source)
;; Older versions don't understand `no-mode'.
(hack-local-variables (when (>= emacs-major-version 26) 'no-mode))
no-byte-compile))))
;; Don't do anything with `no-byte-compile' files (not even load) unless called
;; recursively. Otherwise we might e.g. attempt loading `define-package' and fail.
(if skip-byte-compilation
;; FIXME: Think of a way of not even trying to build such files. Otherwise the
;; output ("ELC blabla.el") is confusing.
(eldev-verbose "Cancelled byte-compilation of `%s': it has `no-byte-compile' local variable" source)
(eldev-verbose (if recursive "Byte-compiling file `%s' early as `require'd from another file..." "Byte-compiling file `%s'...")
source)
(eldev-advised ('load :before
(unless recursive
(lambda (file &rest _ignored)
(eldev--trigger-early-byte-compilation file))))
;; The advice for `load' is, unfortunately, not enough since `require' calls
;; C-level function directly, bypassing advice machinery.
(eldev-advised ('require :before
(unless recursive
(lambda (feature &optional filename &rest _ignored)
(eldev--trigger-early-byte-compilation (or filename (eldev--find-feature-provider feature))))))
(let* (result
(failed-source
(catch 'eldev--byte-compilation-failed
;; Must be within the `catch', because it can trigger byte-compilation
;; of other targets.
(when eldev-build-load-before-byte-compiling
(eldev-trace "Loading file `%s' before byte-compiling it..." source)
(eldev-profile-body
(load source nil t t)))
(setf result (if skip-byte-compilation
'no-byte-compile
;; We are not using `byte-compile-error-on-warn' because that "helpfully"
;; aborts compilation after the very first warning (since forever and at
;; least till June 2022 Emacs snapshots). Makes it tedious to hunt them
;; down if you have twenty.
;;
;; Changes will be done several times in case of a recursive compilation,
;; but this doesn't really matter.
(let* ((original-warning-function (when eldev--have-byte-compile-warning-function byte-compile-log-warning-function))
(byte-compile-log-warning-function (when eldev--have-byte-compile-warning-function
(lambda (string &optional position fill level &rest etc)
(when (and eldev-build-treat-warnings-as-errors (eq level :warning))
(push source (car eldev--recursive-elevated-errors-in))
(setf level :error))
(unless (and eldev-build-suppress-warnings (eq level :warning))
(apply original-warning-function string position fill level etc))))))
(eldev-advised (#'byte-compile-log-warning :around
;; Not available on old Emacs versions. Basically
;; just duplicating the function above.
(unless eldev--have-byte-compile-warning-function
(lambda (original string &optional fill level &rest etc)
(when (and eldev-build-treat-warnings-as-errors (eq level :warning))
(push source (car eldev--recursive-elevated-errors-in))
(setf level :error))
(unless (and eldev-build-suppress-warnings (eq level :warning))
(apply original string fill level etc)))))
(eldev--silence-file-writing-message (expand-file-name target)
(eldev-backtrace-notch 'eldev
(eldev-profile-body
(and (byte-compile-file source)
(if (memq source (car eldev--recursive-elevated-errors-in))
(progn (delete-file target) nil)
t)))))))))
(cond ((eq result 'no-byte-compile)
(eldev-verbose "Cancelled byte-compilation of `%s': it has `no-byte-compile' local variable" source)
nil)
(result
;; Keep in sync with `eldev--do-byte-compile-.el-on-demand'.
(when eldev-build-load-before-byte-compiling
;; Load ourselves, since `byte-compile-file' calls `load'
;; without `nomessage' parameter. Byte-compiled file should
;; be loaded to replace its slower source we loaded above.
(eldev-trace "Loading the byte-compiled file `%s'..." target)
(eldev-profile-body
(load target nil t t)))
nil)
(t
source)))))
(when failed-source
(if recursive
;; Normal errors would get caught inside Emacs byte-compilation code, so
;; we use tag throwing/catching instead.
(throw 'eldev--byte-compilation-failed failed-source)
(signal 'eldev-build-failed `("Failed to byte-compile `%s'" ,failed-source))))
(unless (eq result 'no-byte-compile)
(let ((history-entry (or (assoc (expand-file-name target eldev-project-dir) load-history)
(assoc (expand-file-name source eldev-project-dir) load-history)))
inherited-targets
provided-feature)
(unless history-entry
;; This would not be needed if we could determine dependencies in any
;; other way. At least we load already byte-compiled file.
(eldev-trace "Loading file `%s' in order to find dependencies..." target)
(load (expand-file-name target eldev-project-dir) nil t t)
(setf history-entry (assoc (expand-file-name target eldev-project-dir) load-history)))
(dolist (entry (cdr history-entry))
(pcase entry
(`(require . ,feature)
(unless provided-feature
(let ((provided-by (eldev--find-feature-provider feature)))
(when (stringp provided-by)
(push `(,feature . ,provided-by) inherited-targets)))))
(`(provide . ,feature)
;; See e.g. `eldev-test-compile-circular-requires-1': after `provide' form
;; ignore all remaining `require's for dependency purposes. Also remove
;; self-dependency entry if it has been added.
(puthash feature source eldev--feature-providers)
(setf inherited-targets (delete `(,feature . ,source) inherited-targets)
provided-feature t))))
(eldev-set-target-dependencies target 'eldev-builder-byte-compile-.el
(mapcar (lambda (entry) `(inherits ,(eldev-replace-suffix (cdr entry) ".el" ".elc")))
inherited-targets))))))))))
(defvar eldev--byte-compile-.el-on-demand-recursing-for nil)
(defun eldev--do-byte-compile-.el-on-demand (source quiet)
(unless (member source eldev--byte-compile-.el-on-demand-recursing-for)
(let ((eldev--byte-compile-.el-on-demand-recursing-for eldev--byte-compile-.el-on-demand-recursing-for)
(target (eldev-replace-suffix source ".el" ".elc")))
(push source eldev--byte-compile-.el-on-demand-recursing-for)
(if (and eldev--build-plan (eq (eldev-build-target-status target) 'planned))
(eldev-build-target target)
;; This initializes build system even if it is already up, but hasn't planned to
;; build the target previously. Happens in `compiled-on-demand' loading mode with
;; nested `require', i.e. is a pretty common situation. Maybe something could be
;; optimized to avoid this.
;;
;; To be consistent with how loading in other modes work, make compilation very
;; quiet here. Warnings and errors will still be shown, though, possibly in the
;; middle of actual project's output — but that is an unavoidable side-effect of
;; on-demand compilation.
(let ((eldev-verbosity-level (if quiet 'quiet eldev-verbosity-level)))
(eldev--do-build (list target) t)
;; Keep in sync with `eldev--byte-compile-.el'. Force-load the byte-compiled
;; file now, to replace raw Lisp functions with faster byte-compiled versions.
(unless eldev-build-load-before-byte-compiling
(load target nil t t)))))))
(defun eldev--trigger-early-byte-compilation (file)
;; See `eldev--find-feature-provider': `file' might not really be a file here.
(when (stringp file)
;; Reset `default-directory', because it can have been changed e.g. when called from
;; inside byte-compilation.
(let ((default-directory eldev-project-dir))
(setf file (file-relative-name file eldev-project-dir))
(unless (or (eldev-external-or-absolute-filename file)
(not (eldev-external-or-absolute-filename (file-relative-name file eldev-cache-dir))))
(setf file (eldev-replace-suffix file ".el" ".elc"))
(when (eq (eldev-build-target-status file) 'planned)
(eldev-build-target file))))))
(defun eldev--do-package (sources targets)
(unless sources
(signal 'eldev-build-failed `("No sources for packaging")))
(let* ((package (eldev-package-descriptor))
(pretended-version (or eldev-package-forced-version (package-desc-version package)))
(package-target (car targets))
(entry-target (cadr targets))
(source-dirs (when eldev-project-source-dirs (eldev-project-source-dirs))))
;; Hardly anyone would make an entry without the package, but let's check.
(when (memq (eldev-build-target-status package-target) '(planned building))
(if (cdr sources)
;; Building a tarball.
(let* ((name-version (eldev--package-name-version))
(name-version-dir (file-name-as-directory name-version))
(working-dir (make-temp-file "eldev-packaging-" t))
(descriptor-file (eldev-package-descriptor-file-name))
(descriptor-entry (concat name-version-dir descriptor-file))
temporary-descriptor
symlinked-dir
files-to-tar)
(if (unless source-dirs
;; If the project has no special source subdirectories, try simply
;; symlinking the whole directory at once.
(condition-case nil
(progn (make-symbolic-link eldev-project-dir (expand-file-name name-version working-dir))
(setf symlinked-dir t))
(file-error)))
(dolist (source sources)
(push (concat name-version-dir source) files-to-tar))
;; Either source directories are involved, or symlinking failed for whatever
;; reason. Create filetree for the package.
(let ((working-dir-pkg (expand-file-name name-version working-dir))
created-dirs)
(mkdir working-dir-pkg)
(dolist (source sources)
(let* ((path-within-source-dir (catch 'found
(dolist (source-dir source-dirs)
(when (file-in-directory-p source source-dir)
(throw 'found (file-relative-name source source-dir))))
(error "File `%s' is to be added to the package, but is not contained in any of `eldev-project-source-dirs'" source)))
(subdirectory (file-name-directory path-within-source-dir)))
(let ((source-abspath (expand-file-name source eldev-project-dir))
(target-abspath (expand-file-name path-within-source-dir working-dir-pkg)))
(when subdirectory
(unless (member subdirectory created-dirs)
(mkdir (file-name-directory target-abspath) t)
(push subdirectory created-dirs)))
(unless (condition-case nil
(progn (add-name-to-file source-abspath target-abspath) t)
(file-error))
(copy-file source-abspath target-abspath)))
(push (concat name-version-dir path-within-source-dir) files-to-tar)))))
(unless (file-exists-p descriptor-file)
(with-temp-file (expand-file-name descriptor-file (expand-file-name name-version-dir working-dir))
(insert "; -*- no-byte-compile: t -*-\n")
(pp `(define-package ,(symbol-name (package-desc-name package)) ,(package-version-join pretended-version)
,(package-desc-summary package)
,(eldev-macroexp-quote (mapcar (lambda (requirement)
`(,(car requirement) ,(package-version-join (cadr requirement))))
(package-desc-reqs package)))
,@(apply #'nconc (mapcar (lambda (extra)
`(,(eldev-macroexp-quote (car extra)) ,(eldev-macroexp-quote (cdr extra))))
(package-desc-extras package))))
(current-buffer)))
(setf temporary-descriptor t))
;; Before Emacs 29, the package manager would be confused if the first entry
;; in the generated tarball is a subdirectory (see commit 8de7995 in Emacs
;; sources). Avoid that by having the package descriptor as the head. Don't
;; care what happens if package tarball is generated by other means, not that
;; a lot of people use subdirectories.
(setf files-to-tar (nreverse files-to-tar))
(if (or temporary-descriptor (not (member descriptor-file sources)))
(push descriptor-entry files-to-tar)
;; As far as I understand, this should always be the case, but let's check
(when (member descriptor-entry files-to-tar)
(setf files-to-tar (cons descriptor-entry (delete descriptor-entry files-to-tar)))))
;; Note that `file-name-as-directory' is important on older Emacs versions,
;; otherwise `tar' will be executed from `/tmp'.
(make-directory (file-name-directory package-target) t)
(let ((default-directory (file-name-as-directory working-dir)))
(eldev-verbose "%s" (eldev-message-enumerate-files "Packaging the following file%s: %s (%d)" sources))
(eldev-call-process (eldev-tar-executable) `("-cf" ,(expand-file-name package-target eldev-project-dir) ,@files-to-tar)
:trace-command-line "Full command line to create package tarball"
:forward-output t
(when (/= exit-code 0)
(signal 'eldev-build-failed `("Failed to create package tarball `%s'" ,package-target)))))
;; Note that if packaging fails, `working-dir' and `descriptor-file' are not
;; deleted. This is intentional, so that failed "remains" can be investigated.
(when (and temporary-descriptor symlinked-dir)
(delete-file descriptor-file))
(delete-directory working-dir t))
;; Single-file package.
;; FIXME: Validate sanity.
(make-directory (file-name-directory package-target) t)
(copy-file (car sources) package-target 'overwrite))
(setf eldev--package-target-generated t))
(when (memq (eldev-build-target-status entry-target) '(planned building))
(eldev-verbose "Generating package archive entry `%s'" entry-target)
(with-temp-file entry-target
(prin1 `(,(package-desc-name package)
. [,pretended-version ,(package-desc-reqs package) ,(package-desc-summary package)
,(if (cdr sources) 'tar 'single) ,(package-desc-extras package)])
(current-buffer))
(insert "\n")))))
(provide 'eldev-build)
;;; eldev-build.el ends here