File: installPackage.m2

package info (click to toggle)
macaulay2 1.21%2Bds-3
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 133,096 kB
  • sloc: cpp: 110,377; ansic: 16,306; javascript: 4,193; makefile: 3,821; sh: 3,580; lisp: 764; yacc: 590; xml: 177; python: 140; perl: 114; lex: 65; awk: 3
file content (871 lines) | stat: -rw-r--r-- 40,399 bytes parent folder | download
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
-- -*- fill-column: 107 -*-
--		Copyright 1993-2002 by Daniel R. Grayson
-- TODO: add regex option to readDirectory
-- TODO: add relative directory to minimizeFilename
-- TODO: generate parent nodes for orphan nodes based on their type
-- TODO: make orphan overview nodes subnodes of the top node
-- TODO: not reentrant yet, see resetCounters

needs "document.m2"
needs "examples.m2"
needs "hypertext.m2"
needs "packages.m2"
needs "printing.m2"
needs "validate.m2"

-----------------------------------------------------------------------------
-- Generate the html documentation
-----------------------------------------------------------------------------

-----------------------------------------------------------------------------
-- Local variables
-----------------------------------------------------------------------------

chkdoc := true
seenErrors   := new MutableHashTable
seenWarnings := new MutableHashTable
hadDocumentationError    := false
numDocumentationErrors   := 0
hadDocumentationWarning  := false
numDocumentationWarnings := 0

-- The default values are set so "(html, Hypertext)" works before Macaulay2Doc is installed.
-- TODO: They should be functions rather than global values
topDocumentTag := null
installPrefix   = applicationDirectory() | "local/"  -- default the installation prefix
installLayout   = Layout#2			     -- the layout of the installPrefix, global for communication to document.m2
htmlDirectory   = ""	      -- relative path to the html directory, depends on the package

  topFileName   = "index.html"	-- top node's filename, constant
indexFileName  := "master.html"	-- file name for master index of topics in a package
  tocFileName  := "toc.html"	-- file name for the table of contents of a package

-----------------------------------------------------------------------------
-- Local utilities
-----------------------------------------------------------------------------

resetCounters := () -> (
    seenErrors   = new MutableHashTable;
    seenWarnings = new MutableHashTable;
    hadDocumentationError  = hadDocumentationWarning  = false;
    numDocumentationErrors = numDocumentationWarnings = 0)

signalDocumentationError   = tag -> not seenErrors#?tag and chkdoc and (
    numDocumentationErrors = numDocumentationErrors + 1;
    hadDocumentationError  = seenErrors#tag = true)

-- also called from document.m2 and html.m2, temporarily
signalDocumentationWarning   = tag -> not seenWarnings#?tag and chkdoc and (
    numDocumentationWarnings = numDocumentationWarnings + 1;
    hadDocumentationWarning  = seenWarnings#tag = true)

runfun := f -> if instance(f, Function) then f() else f

-- returns the Layout index of the installPrefix, equal to 1 or 2
initInstallDirectory := opts -> (
    installPrefix = toAbsolutePath opts.InstallPrefix;
    if not fileExists installPrefix then makeDirectory installPrefix;
    installPrefix = realpath installPrefix;
    installLayoutIndex := detectCurrentLayout installPrefix;
    if installLayoutIndex === null then installLayoutIndex = if opts.SeparateExec then 2 else 1;
    installLayout = Layout#installLayoutIndex;
    installLayoutIndex)

-----------------------------------------------------------------------------
-- htmlFilename
-----------------------------------------------------------------------------
-- determines the normalized filename of a key or tag
htmlFilename = method(Dispatch => Thing)
htmlFilename Thing       := key -> htmlFilename makeDocumentTag key
htmlFilename DocumentTag := tag -> (
    fkey := format tag;
    pkgname := tag.Package;
    basefilename := if fkey === pkgname then topFileName else toFilename fkey | ".html";
    if currentPackage#"pkgname" === pkgname then (layout, prefix) := (installLayout, installPrefix)
    else (
	pkginfo := getPackageInfo pkgname;
	-- we assume, perhaps mistakenly, that this package
	-- will be installed under the same prefix eventually
	(layout, prefix) = if pkginfo === null then (installLayout, installPrefix)
	else (Layout#(pkginfo#"layout index"), pkginfo#"prefix"));
    if layout === null then error("package ", pkgname, " is not installed on the prefixPath");
    tail := replace("PKG", pkgname, layout#"packagehtml") | basefilename;
    assert isAbsolutePath prefix;
    (prefix, tail))					    -- this pair will be processed by toURL(String,String)

-----------------------------------------------------------------------------
-- produce html form of documentation, for Macaulay2 and for packages
-----------------------------------------------------------------------------

checkIsTag := tag -> ( assert(instance(tag, DocumentTag)); tag )

alpha := characters "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
numAnchorsMade := 0
makeAnchors := n -> (
     ret := SPAN apply(take(alpha, {numAnchorsMade, n-1}), c -> ANCHOR{ "id" => c, ""});
     numAnchorsMade = n;
     ret)
anchorsUpTo := entry -> if alpha#?numAnchorsMade and entry >= alpha#numAnchorsMade then makeAnchors length select(alpha, c -> entry >= c)
remainingAnchors := () -> makeAnchors (#alpha)

packageTagList := (pkg, topDocumentTag) -> checkIsTag \ unique nonnull join(
    apply(pairs pkg.Dictionary, (name, sym) ->
	if not match("\\$", name) then makeDocumentTag(sym, Package => pkg)),
    apply(values pkg#"raw documentation", rawdoc -> rawdoc.DocumentTag),
    { topDocumentTag })

-----------------------------------------------------------------------------
-- helper functions for assembleTree
-----------------------------------------------------------------------------

-- assemble this next
ForestNode = new Type of BasicList			    -- list of tree nodes, the descendent list
TreeNode   = new Type of BasicList			    -- first entry is DocumentTag for this node, second entry is a forest node

net ForestNode := x -> stack apply(toList x,net)
net TreeNode   := x -> (
    y := net x#1;
    net x#0 || (stack (height y + depth y : " |  ")) | y)

toDoc := method()
toDoc ForestNode := x -> if #x>0 then UL apply(toList x, y -> toDoc y)
toDoc TreeNode   := x -> DIV nonnull { TOH checkIsTag x#0, toDoc x#1 }

traverse := method()
traverse(ForestNode, Function) := (n, f) -> scan(n, t -> traverse(t, f))
traverse(TreeNode,   Function) := (t, f) -> (f t#0, traverse(t#1, f))

-----------------------------------------------------------------------------

-- a directed acyclic graph of nodes -> subnodes
-- TODO: make this functional
graph := new MutableHashTable

-- a depth-first search
makeTree := (parent, graph, visits, node) -> (
    visits#"parents"#node = if visits#"parents"#?node then append(visits#"parents"#node, parent) else {parent};
    tree := if graph#?node then (
	if #visits#"parents"#node == 1 then new ForestNode from apply(graph#node, child -> makeTree(node, graph, visits, child))
	else if not visits#"repeated"#?node and signalDocumentationError node then visits#"repeated"#node = true)
    else if not visits#"missing"#?node and signalDocumentationError node then visits#"missing"#node = true;
    if tree === null then tree = new ForestNode;
    new TreeNode from {node, tree})

-- pre-compute the list of orphan nodes in a graph, with parent
-- node (typically topDocumentTag) appearing in the beginning
orphanNodes := (parent, graph) -> (
    nonLeaves := set keys graph - set flatten values graph;
    if not nonLeaves#?parent and signalDocumentationWarning parent then printerr("warning: top node ", parent, " not a root");
    if #nonLeaves > 1 then printerr("warning: found ", toString(#nonLeaves - 1), " documentation node(s) not listed as a subnode");
    unique prepend(parent, sort keys nonLeaves))

makeForest := (graph, visits) -> (
    new ForestNode from apply(orphanNodes(topDocumentTag, graph),
	node -> makeTree(topDocumentTag, graph, visits, node)))

-----------------------------------------------------------------------------

local NEXT
local PREV
local UP

markLinks := method()
markLinks ForestNode := x -> (
     for i from 0 to #x-2 do (
	  NEXT#(x#i#0) = checkIsTag x#(i+1)#0;
	  PREV#(x#(i+1)#0) = checkIsTag x#i#0;
	  );
     scan(x,markLinks))
markLinks TreeNode   := x -> (
     scan(x#1, i -> UP#(i#0) = checkIsTag x#0);
     markLinks x#1)

buildLinks := method()
buildLinks ForestNode := x -> (
     UP = new MutableHashTable;
     NEXT = new MutableHashTable;
     PREV = new MutableHashTable;
     markLinks x)

-----------------------------------------------------------------------------
-- constructs the tree-structure for the Subnodes of each node
-----------------------------------------------------------------------------

assembleTree := (pkg, nodes) -> (
    resetCounters();
    -- keep track of various possible issues with the nodes
    visits := new HashTable from {
	"parents"  => new MutableHashTable,
	"missing"  => new MutableHashTable,
	"repeated" => new MutableHashTable,
	};
    -- collect links from each tag to its subnodes
    graph = new HashTable from apply(nodes, tag -> (
	    checkIsTag tag;
	    fkey := format tag;
	    if  pkg#"raw documentation"#?fkey
	    and pkg#"raw documentation"#fkey.?Subnodes then (
		subnodes := pkg#"raw documentation"#fkey.Subnodes;
		subnodes  = select(deepApply(subnodes, identity), DocumentTag);
		subnodes  = select(fixup \ subnodes, node -> package node === pkg);
		tag => getPrimaryTag \ subnodes)
	    else tag => {}));
    -- build the forest
    tableOfContents := makeForest(graph, visits);
    -- signal errors
    if chkdoc and hadDocumentationError then (
	scan(keys visits#"missing",
	    node -> (
		printerr("error: missing reference(s) to subnode documentation: ", format node);
		printerr("  Parent nodes: ", demark_", " (format \ unique visits#"parents"#node))));
	scan(keys visits#"repeated",
	    node -> (
		printerr("error: repeated references to subnode documentation: ", format node);
		printerr("  Parent nodes: ", demark_", " (format \ unique visits#"parents"#node))));
	error("installPackage: error in assembling the documentation tree"));
    -- build the navigation links
    buildLinks tableOfContents;
    tableOfContents)

-----------------------------------------------------------------------------
-- Buttons for the top
-----------------------------------------------------------------------------

FORWARD0  := tag -> if NEXT#?tag then NEXT#tag else if UP#?tag then FORWARD0 UP#tag
FORWARD   := tag -> if graph#?tag and length graph#tag > 0 then          first graph#tag else FORWARD0 tag
BACKWARD0 := tag -> if graph#?tag and length graph#tag > 0 then BACKWARD0 last graph#tag else tag
BACKWARD  := tag -> if PREV#?tag then BACKWARD0 PREV#tag else if UP#?tag then UP#tag

-- buttons at the top
topNodeButton  := (htmlDirectory, topFileName)   -> HREF {htmlDirectory | topFileName,   "top" };
indexButton    := (htmlDirectory, indexFileName) -> HREF {htmlDirectory | indexFileName, "index"};
tocButton      := (htmlDirectory, tocFileName)   -> HREF {htmlDirectory | tocFileName,   "toc"};
homeButton     := HREF {"http://macaulay2.com/", "Macaulay2 website"};

nextButton     := tag -> if NEXT#?tag then HREF { htmlFilename NEXT#tag, "next" }     else "next"
prevButton     := tag -> if PREV#?tag then HREF { htmlFilename PREV#tag, "previous" } else "previous"
forwardButton  := tag -> ( f :=  FORWARD tag; if f =!= null then HREF { htmlFilename f, "forward" }  else "forward" )
backwardButton := tag -> ( b := BACKWARD tag; if b =!= null then HREF { htmlFilename b, "backward" } else "backward" )
upButton       := tag -> if   UP#?tag then HREF { htmlFilename   UP#tag, "up" } else "up"
topButton      := tag -> if tag =!= topDocumentTag then topNodeButton(htmlDirectory, topFileName) else "top"

-- TODO: revamp this using Bootstrap
buttonBar := tag -> TABLE { "class" => "buttons", TR { TD { DIV splice between_" | " {
		nextButton tag, prevButton tag, forwardButton tag, backwardButton tag, upButton tag, topButton tag,
		indexButton(htmlDirectory, indexFileName), tocButton(htmlDirectory, tocFileName), homeButton}}}}

-----------------------------------------------------------------------------
-- makePackageIndex
-----------------------------------------------------------------------------

star := IMG {
    "src" => prefixDirectory | replace("PKG", "Style", currentLayout#"package") | "GoldStar.png",
    "alt" => "a gold star"}

findDocumentationPaths := path -> (
    unique nonnull flatten apply(path, dir -> (
	    dir = if isDirectory dir then realpath dir else return;
	    apply(keys Layout, i -> (
		    packagesdir := Layout#i#"packages";
		    if not match(packagesdir | "$", dir) then return;
		    prefix := substring(dir, 0, #dir - #packagesdir);
		    (prefix, i))))))

-- TODO: this function runs on startup, unless -q is given, and takes about 0.1~0.2s
makePackageIndex = method(Dispatch => Thing)
makePackageIndex Sequence := x -> if x === () then makePackageIndex path else error "expected no arguments"
-- this might get too many files (formerly we used packagePath)
makePackageIndex List := path -> (
    verboseLog := if debugLevel > 10 then printerr else identity;
    -- TO DO : rewrite this function to use the results of tallyInstalledPackages
    tallyInstalledPackages();
    initInstallDirectory options installPackage;
    docdirs := toSequence findDocumentationPaths path;
    htmlDirectory = applicationDirectory();
    indexFilename := htmlDirectory | topFileName;
    verboseLog("making index of installed packages in ", indexFilename);
    indexFilename << html validate HTML {
	defaultHEAD { "Macaulay2" },
	BODY {
	    PARA {"This is the directory for Macaulay2 and its packages. Bookmark this page for future reference,
		or run the ", TT "viewHelp", " command in Macaulay2 to open up your browser on this page.
		See the ", homeButton, " for the latest version."},
	    HEADER3 "Documentation",
	    UL nonnull splice {
		if prefixDirectory =!= null then (
		    m2doc := prefixDirectory | replace("PKG", "Macaulay2Doc", currentLayout#"packagehtml") | topFileName;
		    if fileExists m2doc then LI HREF { m2doc, "Macaulay2 documentation" }),
		apply(docdirs, dirs -> (
			prefixDirectory := first dirs;
			layout := Layout#(last dirs);
			docdir := prefixDirectory | layout#"docdir";
			verboseLog("checking documentation directory ", docdir);
			pkghtmldir := pkgname -> prefixDirectory | replace("PKG", pkgname, layout#"packagehtml");
			contents := select(readDirectory docdir, fn -> fn != "." and fn != ".." );
			contents  = select(contents, pkg -> fileExists(pkghtmldir pkg | topFileName));
			if #contents > 0 then LI {
			    HEADER3 {"Packages in ", TT toAbsolutePath prefixDirectory},
			    UL apply(sort contents, pkgname -> (
				    pkgopts := readPackage pkgname;
				    LI nonnull splice {
					HREF { pkghtmldir pkgname | topFileName, pkgname }, -- TO (pkgname | "::" | pkgname),
					if pkgopts.Certification =!= null then (" ", star),
					if pkgopts.Headline      =!= null then commentize pkgopts.Headline}
				    ))
			    }))
		}}} << endl << close;
    htmlDirectory = null;)

-----------------------------------------------------------------------------
-- install PDF documentation for package
-----------------------------------------------------------------------------
-- see book.m2

-----------------------------------------------------------------------------
-- install info documentation for package
-----------------------------------------------------------------------------

upto30 := t -> concatenate(t, 30-#t : " ");

installInfo := (pkg, installPrefix, installLayout, verboseLog) -> (
    topNodeName := format makeDocumentTag(pkg#"pkgname", Package => pkg);

    infoTagConvert' := n -> if topNodeName === n     then "Top" else infoTagConvert n;
    chkInfoKey      := n -> if topNodeName === "Top" then n else if n === "Top" then error "installPackage: encountered a documentation node named 'Top'";
    chkInfoTag      := t -> if package t =!= pkg     then error("installPackage: alien entry in table of contents: ", toString t);

    pushvar(symbol printWidth, 79);

    infotitle       := pkg#"pkgname";
    infobasename    := infotitle | ".info";
    infodir         := installPrefix | installLayout#"info";

    verboseLog("making info file ", infodir | infobasename);
    makeDirectory infodir;
    infofile := openOut(infodir | infobasename);

    infofile << " -*- coding: utf-8 -*- This is " << infobasename << ", produced by Macaulay2, version " << version#"VERSION" << endl << endl;
    infofile << "INFO-DIR-SECTION " << pkg.Options.InfoDirSection << endl;
    infofile << "START-INFO-DIR-ENTRY" << endl;
    infofile << upto30 concatenate( "* ", infotitle, ": (", infotitle, ").") << "  " << pkg.Options.Headline << endl;
    infofile << "END-INFO-DIR-ENTRY" << endl << endl;

    byteOffsets := new MutableHashTable;
    traverse(unbag pkg#"table of contents", tag -> (
	    currentDocumentTag = tag; -- for debugging purposes
	    fkey := format tag;
	    chkInfoTag tag;
	    chkInfoKey fkey;
	    byteOffsets# #byteOffsets = concatenate("Node: ", infoTagConvert' fkey, "\177", toString fileLength infofile);
	    infofile << "\037" << endl << "File: " << infobasename << ", Node: " << infoTagConvert' fkey;
	    if NEXT#?tag then infofile << ", Next: " << infoTagConvert' format NEXT#tag;
	    if PREV#?tag then infofile << ", Prev: " << infoTagConvert' format PREV#tag;
	    -- nodes without an Up: link tend to make the emacs info reader unable to construct the table of contents,
	    -- and the display isn't as nice after that error occurs
	    infofile << ", Up: " << if UP#?tag then infoTagConvert' format UP#tag else
		if fkey == infotitle then "(dir)" else "Top";
	    infofile << endl << endl << info fetchProcessedDocumentation(pkg, fkey) << endl));
    infofile << "\037" << endl << "Tag Table:" << endl;
    scan(values byteOffsets, b -> infofile << b << endl);
    infofile << "\037" << endl << "End Tag Table" << endl;
    infofile << close;

    popvar(symbol printWidth))

-----------------------------------------------------------------------------
-- install HTML documentation for package
-----------------------------------------------------------------------------

upAncestors := tag -> reverse(
    n := 0; prepend(tag, while UP#?tag and n < 20 list (n = n+1; tag = UP#tag)))

makeSortedIndex := (nodes, verboseLog) -> (
     numAnchorsMade = 0;
     fn := installPrefix | htmlDirectory | indexFileName;
     title := format topDocumentTag | " : Index";
     verboseLog("making ", format title, " in ", fn);
     fn << html validate HTML {
	  defaultHEAD title,
	  BODY nonnull {
	       DIV { topNodeButton(htmlDirectory, topFileName), " | ", tocButton(htmlDirectory, tocFileName), -* " | ", directoryButton, *- " | ", homeButton },
	       HR{},
	       HEADER1 title,
	       DIV between(LITERAL "&nbsp;&nbsp;&nbsp;",apply(alpha, c -> HREF {"#"|c, c})),
	       if #nodes > 0 then UL apply(sort select(nodes, tag -> not isUndocumented tag), (tag) -> (
			 checkIsTag tag;
			 anch := anchorsUpTo tag;
			 if anch === null then LI TOH tag else LI {anch, TOH tag})),
	       DIV remainingAnchors()
	       }} << endl << close)

makeTableOfContents := (pkg, verboseLog) -> (
     fn := installPrefix | htmlDirectory | tocFileName;
     title := format topDocumentTag | " : Table of Contents";
     verboseLog("making  ", format title, " in ", fn);
     fn << html validate HTML {
	  defaultHEAD title,
	  BODY {
	       DIV { topNodeButton(htmlDirectory, topFileName), " | ", indexButton(htmlDirectory, indexFileName), -* " | ", directoryButton, *- " | ", homeButton },
	       HR{},
	       HEADER1 title,
	       toDoc unbag pkg#"table of contents"
	       }
	  } << endl << close)

installHTML := (pkg, installPrefix, installLayout, verboseLog, rawDocumentationCache, opts) -> (
    topDocumentTag := makeDocumentTag(pkg#"pkgname", Package => pkg);
    nodes := packageTagList(pkg, topDocumentTag);

    htmlDirectory = replace("PKG", pkg#"pkgname", installLayout#"packagehtml");

    makeDirectory(installPrefix | htmlDirectory);
    verboseLog("making html pages in ", minimizeFilename installPrefix | htmlDirectory);
    if pkg.Options.Certification =!= null then
    (installPrefix | htmlDirectory | ".Certification") << toExternalString pkg.Options.Certification << close;
    (installPrefix | htmlDirectory | ".Headline") << pkg.Options.Headline << close;
    for n in (topFileName, indexFileName, tocFileName) do (
	fn := installPrefix | htmlDirectory | n;
	if fileExists fn then (
	    verboseLog("creating empty html page ", minimizeFilename fn);
	    fn << close)
	else verboseLog("html page exists: ", minimizeFilename fn));
    scan(nodes, tag -> if not isUndocumented tag then (
	    currentDocumentTag = tag; -- for debugging purposes
	    fkey := format tag;
	    fn := concatenate htmlFilename tag;
	    if isSecondaryTag tag
	    or fileExists fn and fileLength fn > 0 and not opts.RemakeAllDocumentation and rawDocumentationCache#?fkey then return;
	    verboseLog("making html page for ", toString tag);
	    fn << html validate HTML {
		defaultHEAD {fkey, commentize headline fkey},
		BODY {
		    buttonBar tag,
		    if UP#?tag
		    then DIV between(" > ", apply(upAncestors tag, i -> TO i))
		    else DIV (TO topDocumentTag, " :: ", TO tag),
		    HR{},
		    fetchProcessedDocumentation(pkg, fkey)
		    }
		} << endl << close));

    -- make the alphabetical index
    makeSortedIndex(nodes, verboseLog);

    -- make the table of contents
    makeTableOfContents(pkg, verboseLog);
    )

-----------------------------------------------------------------------------
-- helper functions for installPackage
-----------------------------------------------------------------------------

reproduciblePaths = outstr -> (
     if topSrcdir === null then return outstr;
     srcdir := regexQuote toAbsolutePath topSrcdir;
     prefixdir := regexQuote prefixDirectory;
     builddir := replace("usr-dist/?$", "", prefixdir);
     homedir := replace("/$", "", regexQuote homeDirectory);
     if any({srcdir, builddir, homedir}, dir -> match(dir, outstr))
     then (
	 -- .m2 files in source directory
	 outstr = replace(srcdir | "Macaulay2/\\b(m2|Core)\\b",
	     finalPrefix | Layout#1#"packages" | "Core", outstr);
	 outstr = replace(srcdir | "Macaulay2/packages/",
	     finalPrefix | Layout#1#"packages", outstr);
	 -- generated .m2 files in build directory (tvalues.m2)
	 outstr = replace(builddir | "Macaulay2/m2",
	     finalPrefix | Layout#1#"packages" | "Core", outstr);
	 -- everything in staging area
	 scan({"bin", "data", "lib", "program licenses", "programs"}, key ->
	     outstr = replace(prefixdir | Layout#2#key,
		 finalPrefix | Layout#1#key, outstr));
	 outstr = replace(prefixdir, finalPrefix, outstr);
	 -- usr-build/bin is in PATH during build
	 outstr = replace(builddir | "usr-build/", finalPrefix, outstr);
	 -- home directory
	 outstr = replace(homedir, "/home/m2user", outstr);
	 );
     outstr
    )

generateExampleResults := (pkg, rawDocumentationCache, exampleDir, exampleOutputDir, verboseLog, pkgopts, opts) -> (
    fnbase := temporaryFileName();
    inpfn  := fkey ->           fnbase | toFilename fkey | ".m2";
    outfn' := fkey ->       exampleDir | toFilename fkey | ".out";
    outfn  := fkey -> exampleOutputDir | toFilename fkey | ".out";
    errfn  := fkey -> exampleOutputDir | toFilename fkey | ".errors";
    gethash := outf -> (
	f := get outf;
	-- this regular expression detects the format used in runFile
	m := regex("\\`.* hash: *(-?[0-9]+)", f);
	if m =!= null then value substring(m#1, f));
    changeFunc := fkey -> () -> remove(rawDocumentationCache, fkey);

    possiblyCache := (outf, outf', fkey) -> () -> (
	if opts.CacheExampleOutput =!= false and pkgopts.CacheExampleOutput === true
	and ( not fileExists outf' or fileExists outf' and fileTime outf > fileTime outf' ) then (
	    verboseLog("caching example results for ", fkey, " in ", outf');
	    if not isDirectory exampleDir then makeDirectory exampleDir;
	    copyFile(outf, outf', Verbose => true)));

    cmphash := (cachef, inputhash) -> (
	cachehash := gethash cachef;
	samehash := cachehash === inputhash;
	if not samehash then verboseLog("warning: cached example file " |
	    cachef | " does not have expected hash" | newline |
	    "expected: " | toString inputhash |", actual: " |
	    toString cachehash);
	samehash);

    usermode := if opts.UserMode === null then not noinitfile else opts.UserMode;
    scan(pairs pkg#"example inputs", (fkey, inputs) -> (
	    inpf  := inpfn  fkey; -- input file
	    outf' := outfn' fkey; -- cached file
	    outf  := outfn  fkey; -- output file
	    errf  := errfn  fkey; -- error file
	    desc  := "example results for " | format fkey;
	    data  := if pkg#"example data files"#?fkey then pkg#"example data files"#fkey else {};
	    inputhash := hash inputs;
	    -- use cached example results
	    if  not opts.RunExamples
	    or  not opts.RerunExamples and fileExists outf  and cmphash(outf, inputhash) then (
		(possiblyCache(outf, outf', fkey))())
	    -- use distributed example results
	    else if pkgopts.UseCachedExampleOutput
	    and not opts.RerunExamples and fileExists outf' and cmphash(outf', inputhash) then (
		if fileExists errf then removeFile errf;
		copyFile(outf', outf);
		verboseLog("using cached " | desc)
		)
	    -- run and capture example results
	    else if elapsedTime captureExampleOutput(
		desc, demark_newline inputs, pkg,
		inpf, outf, errf, data,
		inputhash, changeFunc fkey,
		usermode) then (possiblyCache(outf, outf', fkey))();
	    storeExampleOutput(pkg, fkey, outf, verboseLog)));

    -- check for obsolete example output files and remove them
    if chkdoc then (
	exampleOutputFiles := set apply(keys pkg#"example inputs", outfn);
	scan(readDirectory exampleOutputDir, fn -> (
		fn = exampleOutputDir | fn;
		if match("\\.out$", fn) and not exampleOutputFiles#?fn then removeFile fn)));
    )

getErrors = fn -> (
    -- show several lines before the error and everything afterward
    err := get fn;
    pat := ///(internal error|:[0-9][0-9]*:[0-9][0-9]*:\([0-9][0-9]*\):|/// |
	///^GC|^0x|^out of mem|non-zero status|^Command terminated|/// |
	///user.*system.*elapsed|^[0-9]+\.[0-9]+user|SIGSEGV| \*\*\* )///;
    m := regex(///(.*\n){0,9}.*/// | pat | ///(.*\n)*///, err);
    if m =!= null then substring(m#0, err) else get("!tail " | fn)
    )

-----------------------------------------------------------------------------
-- installPackage
-----------------------------------------------------------------------------

installPackage = method(
    TypicalValue => Package,
    Options => {
	-- overrides the value specified by newPackage if true or false
	CacheExampleOutput     => null,
	CheckDocumentation     => true,
	DebuggingMode          => null,
	FileName               => null,
	IgnoreExampleErrors    => false,
	InstallPrefix          => applicationDirectory() | "local/",
	MakeDocumentation      => true,
	MakeHTML               => true,
	MakeInfo               => true,
	MakePDF                => false,
	MakeLinks              => true,
	-- until we get better dependency graphs between documentation
	-- nodes, "false" here will confuse users
	RemakeAllDocumentation => true,
	RerunExamples          => false,
	RunExamples            => true,
	SeparateExec           => false,
	UserMode               => null,
	Verbose                => false
	})

installPackage String := opts -> pkg -> (
    -- if pkg =!= "Macaulay2Doc" then needsPackage "Macaulay2Doc";  -- load the core documentation
    -- -- we load the package even if it's already been loaded, because even if it was loaded with
    -- -- its documentation the first time, it might have been loaded at a time when the core documentation
    -- -- in the "Macaulay2Doc" package was not yet loaded
    -- ... but we want to build the package Style without loading any other packages
    pkg = loadPackage(pkg,
	FileName          => opts.FileName,
	DebuggingMode     => opts.DebuggingMode,
	LoadDocumentation => opts.MakeDocumentation,
	Reload            => true);
    installPackage(pkg, opts))

installPackage Package := opts -> pkg -> (
    tallyInstalledPackages();
    verboseLog := if opts.Verbose or debugLevel > 0 then printerr else identity;

    use pkg;
    -- TODO: make this more functional
    chkdoc = opts.CheckDocumentation;			    -- oops, this will have a lingering effect...

    if opts.MakeDocumentation and pkg#?"documentation not loaded"
    then pkg = loadPackage(pkg#"pkgname",
	FileName          => opts.FileName,
	DebuggingMode     => opts.DebuggingMode,
	LoadDocumentation => true);
    pkgopts := options pkg;
    if pkgopts.Headline === null or pkgopts.Headline === ""
    then error("installPackage: expected non-empty Headline in package ", toString pkg);

    pushvar(symbol currentPackage, pkg);
    topDocumentTag = makeDocumentTag(pkg#"pkgname", Package => pkg);

    -- set installPrefix, installLayout, and installLayoutIndex
    installLayoutIndex := initInstallDirectory opts; -- TODO: make installPrefix and installLayout non-global
    verboseLog("installing package ", toString pkg, " in ", minimizeFilename installPrefix, " with layout #", toString installLayoutIndex);

    currentSourceDir := pkg#"source directory";

    if currentSourceDir === installPrefix | installLayout#"packages"
    then error("installPackage: the package is already installed and loaded from ", minimizeFilename currentSourceDir);

    verboseLog("using package sources found in ", minimizeFilename currentSourceDir);

    -- copy package source file
    makeDirectory(installPrefix | installLayout#"packages");
    bn := pkg#"pkgname" | ".m2";
    fn := currentSourceDir | bn;
    if not fileExists fn then error("installPackage: file ", fn, " not found");
    copyFile(fn, installPrefix | installLayout#"packages" | bn,
	Verbose => debugLevel > 5);

    -- copy package auxiliary files
    if pkg =!= Core then (
	srcDirectory := replace("PKG", pkg#"pkgname", installLayout#"package");
	auxiliaryFilesDirectory := realpath currentSourceDir | pkg#"pkgname";
	if isDirectory auxiliaryFilesDirectory then (
	    if not pkgopts.AuxiliaryFiles
	    then error("installPackage: package ", toString pkg," has auxiliary files in ",
		format auxiliaryFilesDirectory, ", but newPackage wasn't given AuxiliaryFiles => true");
	    verboseLog("copying auxiliary source files from ", auxiliaryFilesDirectory);
	    makeDirectory(installPrefix | srcDirectory);
	    copyDirectory(auxiliaryFilesDirectory, installPrefix | srcDirectory,
		Exclude    => {"^CVS$", "^\\.svn$", "Makefile"},
		UpdateOnly => true,
		Verbose    => opts.Verbose or debugLevel > 0))
	else if pkgopts.AuxiliaryFiles
	then error("installPackage: package ", toString pkg, " has no directory of auxiliary files, but newPackage was given AuxiliaryFiles => true"));

    if opts.MakeDocumentation then (
	-- here's where we get the list of nodes from the raw documentation
	nodes := packageTagList(pkg, topDocumentTag);

	pkg#"package prefix" = installPrefix;

	-- copy package doc subdirectory if we loaded the package from a distribution
	-- ... to be implemented, but we seem to be copying the examples already, but only partially

	-- cache raw documentation in database, and check for changes
	rawDocumentationCache := new MutableHashTable;
	rawdbname    := databaseFilename(installLayout, pkg#"package prefix", pkg#"pkgname");
	rawdbnametmp := rawdbname | ".tmp";
	verboseLog("storing raw documentation in ", minimizeFilename rawdbname);
	makeDirectory databaseDirectory(installLayout, pkg#"package prefix", pkg#"pkgname");
	if fileExists rawdbnametmp then removeFile rawdbnametmp;
	if fileExists rawdbname then (
	    tmp := openDatabase rawdbname;   -- just to make sure the database file isn't open for writing
	    copyFile(rawdbname, rawdbnametmp);
	    close tmp);
	rawdocDatabase := openDatabaseOut rawdbnametmp;
	rawDoc := pkg#"raw documentation";
	-- remove any keys from the processed database no longer used
	scan(keys rawdocDatabase - set keys rawDoc, key -> remove(rawdocDatabase, key));
	scan(nodes, tag -> (
		fkey := format tag;
		if rawDoc#?fkey then (
		    v := evaluateWithPackage(getpkg "Text", rawDoc#fkey, toExternalString);
		    if rawdocDatabase#?fkey
		    then if rawdocDatabase#fkey === v then rawDocumentationCache#fkey = true else rawdocDatabase#fkey = v
		    else (
			rawdocDatabase#fkey = v;
			verboseLog("new raw documentation, not already in database, for ", fkey)))
		else if rawdocDatabase#?fkey
		then printerr("warning: raw documentation for ", fkey, ", in database, is no longer present")
		else rawDocumentationCache#fkey = true;
		));
	close rawdocDatabase;
	verboseLog "closed the database";

	-- run tests that are functions
	-- TODO: is this used anywhere?
	verboseLog "running tests that are functions";
	scan(pairs pkg#"test inputs", (key, str) -> if instance(str, Function) then (
		verboseLog("  running test ", key, ", function ", str);
		str()));

	-- directories for cached and generated example outputs
	exampleDir := realpath currentSourceDir | pkg#"pkgname" | "/examples" | "/";
	exampleOutputDir := installPrefix | replace("PKG", pkg#"pkgname", installLayout#"packageexampleoutput");
	makeDirectory exampleOutputDir;

	-- make example output files, or else copy them from old package directory tree
	verboseLog("making example result files in ", minimizeFilename exampleOutputDir);
	(hadError, numErrors) = (false, 0); -- declared in run.m2
	generateExampleResults(pkg, rawDocumentationCache, exampleDir, exampleOutputDir, verboseLog, pkgopts, opts);

	if hadError then (
	    errmsg := ("installPackage: ", toString numErrors, " error(s) occurred running examples for package ", pkg#"pkgname",
	    if opts.Verbose or debugLevel > 0 then ":" | newline | newline |
	    concatenate apply(select(readDirectory exampleOutputDir, file -> match("\\.errors$", file)), err ->
		err | newline |	concatenate(width err : "*") | newline | getErrors(exampleOutputDir | err)) else "");
	    if opts.IgnoreExampleErrors
	    then stderr << " -- warning: " << concatenate errmsg << endl
	    else error errmsg);

	-- if no examples were generated, then remove the directory
	if length readDirectory exampleOutputDir == 2 then removeDirectory exampleOutputDir;

	-- process documentation
	verboseLog "processing documentation nodes...";
	-- ~50s -> ~100s for Macaulay2Doc
	scan(nodes, tag ->
	    if      isUndocumented tag              then verboseLog("undocumented ", toString tag)
	    else if isSecondaryTag tag              then verboseLog("is secondary ", toString tag)
	    else if not opts.RemakeAllDocumentation
	    and     not opts.MakeInfo -- when making the info file, we need to process all the documentation
	    and rawDocumentationCache#?(format tag) then verboseLog("skipping     ", toString tag)
	    else storeProcessedDocumentation(pkg, tag, opts, verboseLog));

	-- should this be here, or farther up? Note: assembleTree resets the counters, so stay above that.
	if chkdoc and hadDocumentationError then error(
	    toString numDocumentationErrors, " errors(s) occurred in processing documentation for package ", toString pkg);

	if pkg#?rawKeyDB and isOpen pkg#rawKeyDB then close pkg#rawKeyDB;

	shield ( moveFile(rawdbnametmp, rawdbname, Verbose => debugLevel > 1); );

	pkg#rawKeyDB = openDatabase rawdbname;
	addEndFunction(() -> if pkg#?rawKeyDB and isOpen pkg#rawKeyDB then close pkg#rawKeyDB);

	-- make table of contents, including next, prev, and up links
	verboseLog("assembling table of contents");
	tableOfContents := assembleTree(pkg, getPrimaryTag \ select(nodes, tag -> not isUndocumented tag));
	-- if chkdoc then stderr << "+++++" << endl << "table of contents, in tree form:" << endl << tableOfContents << endl << "+++++" << endl;
	pkg#"table of contents" = Bag {tableOfContents}; -- we bag it because it might be big!
	pkg#"links up"   = UP;
	pkg#"links next" = NEXT;
	pkg#"links prev" = PREV;

	-- check that everything is documented
	-- ~22s for Macaulay2Doc
	if chkdoc then (
	    resetCounters();
	    srcpkg := if pkg#"pkgname" == "Macaulay2Doc" then Core else pkg;
	    scan(join (srcpkg#"exported symbols", srcpkg#"exported mutable symbols"), s -> (
		    tag := makeDocumentTag s;
		    if  not isUndocumented tag
		    and not hasDocumentation tag
		    and signalDocumentationWarning tag then printerr(
			"warning: symbol has no documentation: ", toString tag, ", package ", toString package tag);
		    f := value s;
		    if instance(f, Function) then (
			scan(methods f, m -> if isDocumentableMethod m then (
				tag := makeDocumentTag m;
				if  not isUndocumented tag
				and not hasDocumentation tag
				and signalDocumentationWarning tag then printerr(
				    "warning: method has no documentation: ", toString tag,
				    ", key ", toExternalString tag.Key,
				    ", package ", toString package tag);
				));
			))));

	if chkdoc and hadDocumentationError then error(
	    toString numDocumentationErrors, " errors(s) occurred in documentation for package ", toString pkg);

	-- make info documentation
	-- ~60 -> ~70s for Macaulay2Doc
	if opts.MakeInfo then installInfo(pkg, installPrefix, installLayout, verboseLog)
	else verboseLog("not making documentation in info format");

	-- make html documentation
	-- ~50 -> ~80s for Macaulay2Doc
	if opts.MakeHTML then installHTML(pkg, installPrefix, installLayout, verboseLog, rawDocumentationCache, opts)
	else verboseLog("not making documentation in HTML format");

	-- make pdf documentation
	if opts.MakePDF then installPDF(pkg, installPrefix, installLayout, verboseLog)
	else verboseLog("not making documentation in PDF format");

	if chkdoc and hadDocumentationWarning then printerr("warning: ",
	    toString numDocumentationWarnings, " warning(s) occurred in documentation for package ", toString pkg);

	); -- end of opts.MakeDocumentation

    -- touch .installed if no errors occurred
    if not hadError then (
	libDir := pkg#"package prefix" | replace("PKG", pkg#"pkgname", installLayout#"packagelib");
	iname := libDir|".installed";
	if not fileExists libDir then makeDirectory libDir;
	iname << close;
	verboseLog("file created: ", minimizeFilename iname);
	);

    verboseLog("installed package ", toString pkg, " in ", installPrefix);
    popvar(symbol currentPackage);
    if not noinitfile then makePackageIndex();
    htmlDirectory = null;
    installLayout = null;
    installPrefix = null;
    tallyInstalledPackages();
    pkg)

-----------------------------------------------------------------------------
-- installedPackages
-----------------------------------------------------------------------------

installedPackages = () -> (
    prefix := applicationDirectory() | "local/";
    layout := Layout#(detectCurrentLayout prefix);
    packages := prefix | layout#"packages";
    if isDirectory packages then for pkg in readDirectory packages list (
	if match("\\.m2$", pkg) then replace("\\.m2$", "", pkg) else continue) else {})

-----------------------------------------------------------------------------
-- uninstallPackage and uninstallAllPackages
-----------------------------------------------------------------------------

removeFiles = p -> scan(reverse findFiles p, fn -> if fileExists fn or readlink fn =!= null then (
	if isDirectory fn then (
	    -- we silently ignore nonempty directories, which could result from
	    -- removing an open file on an NFS file system.  Such files get renamed
	    -- to something beginning with ".nfs".
	    if length readDirectory fn == 2 then removeDirectory fn)
	else removeFile fn))

uninstallPackage = method(Options => { InstallPrefix => applicationDirectory() | "local/" })
uninstallPackage Package := opts -> pkg -> uninstallPackage(toString pkg, opts)
uninstallPackage String  := opts -> pkg -> (
    checkPackageName pkg;
    installPrefix := minimizeFilename opts.InstallPrefix;
    apply(findFiles apply({1, 2},
	    i -> apply(flatten {
		    Layout#i#"packages" | pkg | ".m2", Layout#i#"info" | pkg | ".info",
		    apply({"package", "packagelib", "packagedoc"}, f -> replace("PKG", pkg, Layout#i#f))
		    },
		p -> installPrefix | p)),
	removeFiles);
    tallyInstalledPackages();
    )

uninstallAllPackages = () -> for pkg in installedPackages() do (
    printerr("uninstalling package ", toString pkg); uninstallPackage pkg)

-- Local Variables:
-- compile-command: "make -C $M2BUILDDIR/Macaulay2/m2 "
-- End: