1#! @perl@
2
3use strict;
4use Cwd 'abs_path';
5use File::Spec;
6use File::Path;
7use File::Basename;
8use File::Slurp;
9use File::stat;
10
11umask(0022);
12
13sub uniq {
14 my %seen;
15 my @res = ();
16 foreach my $s (@_) {
17 if (!defined $seen{$s}) {
18 $seen{$s} = 1;
19 push @res, $s;
20 }
21 }
22 return @res;
23}
24
25sub runCommand {
26 my ($cmd) = @_;
27 open FILE, "$cmd 2>&1 |" or die "Failed to execute: $cmd\n";
28 my @ret = <FILE>;
29 close FILE;
30 return ($?, @ret);
31}
32
33# Process the command line.
34my $outDir = "/etc/nixos";
35my $rootDir = ""; # = /
36my $force = 0;
37my $noFilesystems = 0;
38my $showHardwareConfig = 0;
39
40for (my $n = 0; $n < scalar @ARGV; $n++) {
41 my $arg = $ARGV[$n];
42 if ($arg eq "--help") {
43 exec "man nixos-generate-config" or die;
44 }
45 elsif ($arg eq "--dir") {
46 $n++;
47 $outDir = $ARGV[$n];
48 die "$0: ‘--dir’ requires an argument\n" unless defined $outDir;
49 }
50 elsif ($arg eq "--root") {
51 $n++;
52 $rootDir = $ARGV[$n];
53 die "$0: ‘--root’ requires an argument\n" unless defined $rootDir;
54 $rootDir =~ s/\/*$//; # remove trailing slashes
55 }
56 elsif ($arg eq "--force") {
57 $force = 1;
58 }
59 elsif ($arg eq "--no-filesystems") {
60 $noFilesystems = 1;
61 }
62 elsif ($arg eq "--show-hardware-config") {
63 $showHardwareConfig = 1;
64 }
65 else {
66 die "$0: unrecognized argument ‘$arg’\n";
67 }
68}
69
70
71my @attrs = ();
72my @kernelModules = ();
73my @initrdKernelModules = ();
74my @initrdAvailableKernelModules = ();
75my @modulePackages = ();
76my @imports;
77
78
79sub debug {
80 return unless defined $ENV{"DEBUG"};
81 print STDERR @_;
82}
83
84
85my $cpuinfo = read_file "/proc/cpuinfo";
86
87
88sub hasCPUFeature {
89 my $feature = shift;
90 return $cpuinfo =~ /^flags\s*:.* $feature( |$)/m;
91}
92
93
94sub cpuManufacturer {
95 my $id = shift;
96 return $cpuinfo =~ /^vendor_id\s*:.* $id$/m;
97}
98
99
100# Determine CPU governor to use
101if (-e "/sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors") {
102 my $governors = read_file("/sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors");
103 # ondemand governor is not available on sandy bridge or later Intel CPUs
104 my @desired_governors = ("ondemand", "powersave");
105 my $e;
106
107 foreach $e (@desired_governors) {
108 if (index($governors, $e) != -1) {
109 last if (push @attrs, "powerManagement.cpuFreqGovernor = lib.mkDefault \"$e\";");
110 }
111 }
112}
113
114
115# Virtualization support?
116push @kernelModules, "kvm-intel" if hasCPUFeature "vmx";
117push @kernelModules, "kvm-amd" if hasCPUFeature "svm";
118
119push @attrs, "hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;" if cpuManufacturer "AuthenticAMD";
120push @attrs, "hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;" if cpuManufacturer "GenuineIntel";
121
122
123# Look at the PCI devices and add necessary modules. Note that most
124# modules are auto-detected so we don't need to list them here.
125# However, some are needed in the initrd to boot the system.
126
127my $videoDriver;
128
129sub pciCheck {
130 my $path = shift;
131 my $vendor = read_file "$path/vendor"; chomp $vendor;
132 my $device = read_file "$path/device"; chomp $device;
133 my $class = read_file "$path/class"; chomp $class;
134
135 my $module;
136 if (-e "$path/driver/module") {
137 $module = basename `readlink -f $path/driver/module`;
138 chomp $module;
139 }
140
141 debug "$path: $vendor $device $class";
142 debug " $module" if defined $module;
143 debug "\n";
144
145 if (defined $module) {
146 # See the bottom of http://pciids.sourceforge.net/pci.ids for
147 # device classes.
148 if (# Mass-storage controller. Definitely important.
149 $class =~ /^0x01/ ||
150
151 # Firewire controller. A disk might be attached.
152 $class =~ /^0x0c00/ ||
153
154 # USB controller. Needed if we want to use the
155 # keyboard when things go wrong in the initrd.
156 $class =~ /^0x0c03/
157 )
158 {
159 push @initrdAvailableKernelModules, $module;
160 }
161 }
162
163 # broadcom STA driver (wl.ko)
164 # list taken from http://www.broadcom.com/docs/linux_sta/README.txt
165 if ($vendor eq "0x14e4" &&
166 ($device eq "0x4311" || $device eq "0x4312" || $device eq "0x4313" ||
167 $device eq "0x4315" || $device eq "0x4327" || $device eq "0x4328" ||
168 $device eq "0x4329" || $device eq "0x432a" || $device eq "0x432b" ||
169 $device eq "0x432c" || $device eq "0x432d" || $device eq "0x4353" ||
170 $device eq "0x4357" || $device eq "0x4358" || $device eq "0x4359" ||
171 $device eq "0x4331" || $device eq "0x43a0" || $device eq "0x43b1"
172 ) )
173 {
174 push @modulePackages, "config.boot.kernelPackages.broadcom_sta";
175 push @kernelModules, "wl";
176 }
177
178 # broadcom FullMac driver
179 # list taken from
180 # https://wireless.wiki.kernel.org/en/users/Drivers/brcm80211#brcmfmac
181 if ($vendor eq "0x14e4" &&
182 ($device eq "0x43a3" || $device eq "0x43df" || $device eq "0x43ec" ||
183 $device eq "0x43d3" || $device eq "0x43d9" || $device eq "0x43e9" ||
184 $device eq "0x43ba" || $device eq "0x43bb" || $device eq "0x43bc" ||
185 $device eq "0xaa52" || $device eq "0x43ca" || $device eq "0x43cb" ||
186 $device eq "0x43cc" || $device eq "0x43c3" || $device eq "0x43c4" ||
187 $device eq "0x43c5"
188 ) )
189 {
190 # we need e.g. brcmfmac43602-pcie.bin
191 push @imports, "(modulesPath + \"/hardware/network/broadcom-43xx.nix\")";
192 }
193
194 # In case this is a virtio scsi device, we need to explicitly make this available.
195 if ($vendor eq "0x1af4" && $device eq "0x1004") {
196 push @initrdAvailableKernelModules, "virtio_scsi";
197 }
198
199 # Can't rely on $module here, since the module may not be loaded
200 # due to missing firmware. Ideally we would check modules.pcimap
201 # here.
202 push @attrs, "networking.enableIntel2200BGFirmware = true;" if
203 $vendor eq "0x8086" &&
204 ($device eq "0x1043" || $device eq "0x104f" || $device eq "0x4220" ||
205 $device eq "0x4221" || $device eq "0x4223" || $device eq "0x4224");
206
207 push @attrs, "networking.enableIntel3945ABGFirmware = true;" if
208 $vendor eq "0x8086" &&
209 ($device eq "0x4229" || $device eq "0x4230" ||
210 $device eq "0x4222" || $device eq "0x4227");
211
212 # Assume that all NVIDIA cards are supported by the NVIDIA driver.
213 # There may be exceptions (e.g. old cards).
214 # FIXME: do we want to enable an unfree driver here?
215 #$videoDriver = "nvidia" if $vendor eq "0x10de" && $class =~ /^0x03/;
216}
217
218foreach my $path (glob "/sys/bus/pci/devices/*") {
219 pciCheck $path;
220}
221
222# Idem for USB devices.
223
224sub usbCheck {
225 my $path = shift;
226 my $class = read_file "$path/bInterfaceClass"; chomp $class;
227 my $subclass = read_file "$path/bInterfaceSubClass"; chomp $subclass;
228 my $protocol = read_file "$path/bInterfaceProtocol"; chomp $protocol;
229
230 my $module;
231 if (-e "$path/driver/module") {
232 $module = basename `readlink -f $path/driver/module`;
233 chomp $module;
234 }
235
236 debug "$path: $class $subclass $protocol";
237 debug " $module" if defined $module;
238 debug "\n";
239
240 if (defined $module) {
241 if (# Mass-storage controller. Definitely important.
242 $class eq "08" ||
243
244 # Keyboard. Needed if we want to use the
245 # keyboard when things go wrong in the initrd.
246 ($class eq "03" && $protocol eq "01")
247 )
248 {
249 push @initrdAvailableKernelModules, $module;
250 }
251 }
252}
253
254foreach my $path (glob "/sys/bus/usb/devices/*") {
255 if (-e "$path/bInterfaceClass") {
256 usbCheck $path;
257 }
258}
259
260
261# Add the modules for all block and MMC devices.
262foreach my $path (glob "/sys/class/{block,mmc_host}/*") {
263 my $module;
264 if (-e "$path/device/driver/module") {
265 $module = basename `readlink -f $path/device/driver/module`;
266 chomp $module;
267 push @initrdAvailableKernelModules, $module;
268 }
269}
270
271# Add bcache module, if needed.
272my @bcacheDevices = glob("/dev/bcache*");
273if (scalar @bcacheDevices > 0) {
274 push @initrdAvailableKernelModules, "bcache";
275}
276
277# Prevent unbootable systems if LVM snapshots are present at boot time.
278if (`lsblk -o TYPE` =~ "lvm") {
279 push @initrdKernelModules, "dm-snapshot";
280}
281
282my $virt = `systemd-detect-virt`;
283chomp $virt;
284
285
286# Check if we're a VirtualBox guest. If so, enable the guest
287# additions.
288if ($virt eq "oracle") {
289 push @attrs, "virtualisation.virtualbox.guest.enable = true;"
290}
291
292
293# Likewise for QEMU.
294if ($virt eq "qemu" || $virt eq "kvm" || $virt eq "bochs") {
295 push @imports, "(modulesPath + \"/profiles/qemu-guest.nix\")";
296}
297
298# Also for Hyper-V.
299if ($virt eq "microsoft") {
300 push @attrs, "virtualisation.hypervGuest.enable = true;"
301}
302
303
304# Pull in NixOS configuration for containers.
305if ($virt eq "systemd-nspawn") {
306 push @attrs, "boot.isContainer = true;";
307}
308
309
310# Provide firmware for devices that are not detected by this script,
311# unless we're in a VM/container.
312push @imports, "(modulesPath + \"/installer/scan/not-detected.nix\")"
313 if $virt eq "none";
314
315
316# For a device name like /dev/sda1, find a more stable path like
317# /dev/disk/by-uuid/X or /dev/disk/by-label/Y.
318sub findStableDevPath {
319 my ($dev) = @_;
320 return $dev if substr($dev, 0, 1) ne "/";
321 return $dev unless -e $dev;
322
323 my $st = stat($dev) or return $dev;
324
325 foreach my $dev2 (glob("/dev/disk/by-uuid/*"), glob("/dev/mapper/*"), glob("/dev/disk/by-label/*")) {
326 my $st2 = stat($dev2) or next;
327 return $dev2 if $st->rdev == $st2->rdev;
328 }
329
330 return $dev;
331}
332
333push @attrs, "services.xserver.videoDrivers = [ \"$videoDriver\" ];" if $videoDriver;
334
335# Generate the swapDevices option from the currently activated swap
336# devices.
337my @swaps = read_file("/proc/swaps", err_mode => 'carp');
338my @swapDevices;
339if (@swaps) {
340 shift @swaps;
341 foreach my $swap (@swaps) {
342 my @fields = split ' ', $swap;
343 my $swapFilename = $fields[0];
344 my $swapType = $fields[1];
345 next unless -e $swapFilename;
346 my $dev = findStableDevPath $swapFilename;
347 if ($swapType =~ "partition") {
348 # zram devices are more likely created by configuration.nix, so
349 # ignore them here
350 next if ($swapFilename =~ /^\/dev\/zram/);
351 push @swapDevices, "{ device = \"$dev\"; }";
352 } elsif ($swapType =~ "file") {
353 # swap *files* are more likely specified in configuration.nix, so
354 # ignore them here.
355 } else {
356 die "Unsupported swap type: $swapType\n";
357 }
358 }
359}
360
361
362# Generate the fileSystems option from the currently mounted
363# filesystems.
364sub in {
365 my ($d1, $d2) = @_;
366 return $d1 eq $d2 || substr($d1, 0, length($d2) + 1) eq "$d2/";
367}
368
369my $fileSystems;
370my %fsByDev;
371foreach my $fs (read_file("/proc/self/mountinfo")) {
372 chomp $fs;
373 my @fields = split / /, $fs;
374 my $mountPoint = $fields[4];
375 $mountPoint =~ s/\\040/ /g; # account for mount points with spaces in the name (\040 is the escape character)
376 $mountPoint =~ s/\\011/\t/g; # account for mount points with tabs in the name (\011 is the escape character)
377 next unless -d $mountPoint;
378 my @mountOptions = split /,/, $fields[5];
379
380 next if !in($mountPoint, $rootDir);
381 $mountPoint = substr($mountPoint, length($rootDir)); # strip the root directory (e.g. /mnt)
382 $mountPoint = "/" if $mountPoint eq "";
383
384 # Skip special filesystems.
385 next if in($mountPoint, "/proc") || in($mountPoint, "/dev") || in($mountPoint, "/sys") || in($mountPoint, "/run") || $mountPoint eq "/var/lib/nfs/rpc_pipefs";
386
387 # Skip the optional fields.
388 my $n = 6; $n++ while $fields[$n] ne "-"; $n++;
389 my $fsType = $fields[$n];
390 my $device = $fields[$n + 1];
391 my @superOptions = split /,/, $fields[$n + 2];
392 $device =~ s/\\040/ /g; # account for devices with spaces in the name (\040 is the escape character)
393 $device =~ s/\\011/\t/g; # account for mount points with tabs in the name (\011 is the escape character)
394
395 # Skip the read-only bind-mount on /nix/store.
396 next if $mountPoint eq "/nix/store" && (grep { $_ eq "rw" } @superOptions) && (grep { $_ eq "ro" } @mountOptions);
397
398 # Maybe this is a bind-mount of a filesystem we saw earlier?
399 if (defined $fsByDev{$fields[2]}) {
400 # Make sure this isn't a btrfs subvolume.
401 my $msg = `btrfs subvol show $rootDir$mountPoint`;
402 if ($? != 0 || $msg =~ /ERROR:/s) {
403 my $path = $fields[3]; $path = "" if $path eq "/";
404 my $base = $fsByDev{$fields[2]};
405 $base = "" if $base eq "/";
406 $fileSystems .= <<EOF;
407 fileSystems.\"$mountPoint\" =
408 { device = \"$base$path\";
409 fsType = \"none\";
410 options = \[ \"bind\" \];
411 };
412
413EOF
414 next;
415 }
416 }
417 $fsByDev{$fields[2]} = $mountPoint;
418
419 # We don't know how to handle FUSE filesystems.
420 if ($fsType eq "fuseblk" || $fsType eq "fuse") {
421 print STDERR "warning: don't know how to emit ‘fileSystem’ option for FUSE filesystem ‘$mountPoint’\n";
422 next;
423 }
424
425 # Is this a mount of a loopback device?
426 my @extraOptions;
427 if ($device =~ /\/dev\/loop(\d+)/) {
428 my $loopnr = $1;
429 my $backer = read_file "/sys/block/loop$loopnr/loop/backing_file";
430 if (defined $backer) {
431 chomp $backer;
432 $device = $backer;
433 push @extraOptions, "loop";
434 }
435 }
436
437 # Is this a btrfs filesystem?
438 if ($fsType eq "btrfs") {
439 my ($status, @info) = runCommand("btrfs subvol show $rootDir$mountPoint");
440 if ($status != 0 || join("", @info) =~ /ERROR:/) {
441 die "Failed to retrieve subvolume info for $mountPoint\n";
442 }
443 my @ids = join("\n", @info) =~ m/^(?!\/\n).*Subvolume ID:[ \t\n]*([0-9]+)/s;
444 if ($#ids > 0) {
445 die "Btrfs subvol name for $mountPoint listed multiple times in mount\n"
446 } elsif ($#ids == 0) {
447 my @paths = join("", @info) =~ m/^([^\n]*)/;
448 if ($#paths > 0) {
449 die "Btrfs returned multiple paths for a single subvolume id, mountpoint $mountPoint\n";
450 } elsif ($#paths != 0) {
451 die "Btrfs did not return a path for the subvolume at $mountPoint\n";
452 }
453 push @extraOptions, "subvol=$paths[0]";
454 }
455 }
456
457 # Don't emit tmpfs entry for /tmp, because it most likely comes from the
458 # boot.tmpOnTmpfs option in configuration.nix (managed declaratively).
459 next if ($mountPoint eq "/tmp" && $fsType eq "tmpfs");
460
461 # Emit the filesystem.
462 $fileSystems .= <<EOF;
463 fileSystems.\"$mountPoint\" =
464 { device = \"${\(findStableDevPath $device)}\";
465 fsType = \"$fsType\";
466EOF
467
468 if (scalar @extraOptions > 0) {
469 $fileSystems .= <<EOF;
470 options = \[ ${\join " ", map { "\"" . $_ . "\"" } uniq(@extraOptions)} \];
471EOF
472 }
473
474 $fileSystems .= <<EOF;
475 };
476
477EOF
478
479 # If this filesystem is on a LUKS device, then add a
480 # boot.initrd.luks.devices entry.
481 if (-e $device) {
482 my $deviceName = basename(abs_path($device));
483 if (-e "/sys/class/block/$deviceName"
484 && read_file("/sys/class/block/$deviceName/dm/uuid", err_mode => 'quiet') =~ /^CRYPT-LUKS/)
485 {
486 my @slaves = glob("/sys/class/block/$deviceName/slaves/*");
487 if (scalar @slaves == 1) {
488 my $slave = "/dev/" . basename($slaves[0]);
489 if (-e $slave) {
490 my $dmName = read_file("/sys/class/block/$deviceName/dm/name");
491 chomp $dmName;
492 # Ensure to add an entry only once
493 my $luksDevice = " boot.initrd.luks.devices.\"$dmName\".device";
494 if ($fileSystems !~ /^\Q$luksDevice\E/m) {
495 $fileSystems .= "$luksDevice = \"${\(findStableDevPath $slave)}\";\n\n";
496 }
497 }
498 }
499 }
500 }
501}
502
503# For lack of a better way to determine it, guess whether we should use a
504# bigger font for the console from the display mode on the first
505# framebuffer. A way based on the physical size/actual DPI reported by
506# the monitor would be nice, but I don't know how to do this without X :)
507my $fb_modes_file = "/sys/class/graphics/fb0/modes";
508if (-f $fb_modes_file && -r $fb_modes_file) {
509 my $modes = read_file($fb_modes_file);
510 $modes =~ m/([0-9]+)x([0-9]+)/;
511 my $console_width = $1, my $console_height = $2;
512 if ($console_width > 1920) {
513 push @attrs, "# high-resolution display";
514 push @attrs, 'hardware.video.hidpi.enable = lib.mkDefault true;';
515 }
516}
517
518
519# Generate the hardware configuration file.
520
521sub toNixStringList {
522 my $res = "";
523 foreach my $s (@_) {
524 $res .= " \"$s\"";
525 }
526 return $res;
527}
528sub toNixList {
529 my $res = "";
530 foreach my $s (@_) {
531 $res .= " $s";
532 }
533 return $res;
534}
535
536sub multiLineList {
537 my $indent = shift;
538 return " [ ]" if !@_;
539 my $res = "\n${indent}[ ";
540 my $first = 1;
541 foreach my $s (@_) {
542 $res .= "$indent " if !$first;
543 $first = 0;
544 $res .= "$s\n";
545 }
546 $res .= "$indent]";
547 return $res;
548}
549
550my $initrdAvailableKernelModules = toNixStringList(uniq @initrdAvailableKernelModules);
551my $initrdKernelModules = toNixStringList(uniq @initrdKernelModules);
552my $kernelModules = toNixStringList(uniq @kernelModules);
553my $modulePackages = toNixList(uniq @modulePackages);
554
555my $fsAndSwap = "";
556if (!$noFilesystems) {
557 $fsAndSwap = "\n$fileSystems ";
558 $fsAndSwap .= "swapDevices =" . multiLineList(" ", @swapDevices) . ";\n";
559}
560
561my $hwConfig = <<EOF;
562# Do not modify this file! It was generated by ‘nixos-generate-config’
563# and may be overwritten by future invocations. Please make changes
564# to /etc/nixos/configuration.nix instead.
565{ config, lib, pkgs, modulesPath, ... }:
566
567{
568 imports =${\multiLineList(" ", @imports)};
569
570 boot.initrd.availableKernelModules = [$initrdAvailableKernelModules ];
571 boot.initrd.kernelModules = [$initrdKernelModules ];
572 boot.kernelModules = [$kernelModules ];
573 boot.extraModulePackages = [$modulePackages ];
574$fsAndSwap
575${\join "", (map { " $_\n" } (uniq @attrs))}}
576EOF
577
578sub generateNetworkingDhcpConfig {
579 my $config = <<EOF;
580 # The global useDHCP flag is deprecated, therefore explicitly set to false here.
581 # Per-interface useDHCP will be mandatory in the future, so this generated config
582 # replicates the default behaviour.
583 networking.useDHCP = false;
584EOF
585
586 foreach my $path (glob "/sys/class/net/*") {
587 my $dev = basename($path);
588 if ($dev ne "lo") {
589 $config .= " networking.interfaces.$dev.useDHCP = true;\n";
590 }
591 }
592
593 return $config;
594}
595
596sub generateXserverConfig {
597 my $xserverEnabled = "@xserverEnabled@";
598
599 my $config = "";
600 if ($xserverEnabled eq "1") {
601 $config = <<EOF;
602 # Enable the X11 windowing system.
603 services.xserver.enable = true;
604EOF
605 } else {
606 $config = <<EOF;
607 # Enable the X11 windowing system.
608 # services.xserver.enable = true;
609EOF
610 }
611}
612
613if ($showHardwareConfig) {
614 print STDOUT $hwConfig;
615} else {
616 $outDir = "$rootDir$outDir";
617
618 my $fn = "$outDir/hardware-configuration.nix";
619 print STDERR "writing $fn...\n";
620 mkpath($outDir, 0, 0755);
621 write_file($fn, $hwConfig);
622
623 # Generate a basic configuration.nix, unless one already exists.
624 $fn = "$outDir/configuration.nix";
625 if ($force || ! -e $fn) {
626 print STDERR "writing $fn...\n";
627
628 my $bootLoaderConfig = "";
629 if (-e "/sys/firmware/efi/efivars") {
630 $bootLoaderConfig = <<EOF;
631 # Use the systemd-boot EFI boot loader.
632 boot.loader.systemd-boot.enable = true;
633 boot.loader.efi.canTouchEfiVariables = true;
634EOF
635 } elsif (-e "/boot/extlinux") {
636 $bootLoaderConfig = <<EOF;
637 # Use the extlinux boot loader. (NixOS wants to enable GRUB by default)
638 boot.loader.grub.enable = false;
639 # Enables the generation of /boot/extlinux/extlinux.conf
640 boot.loader.generic-extlinux-compatible.enable = true;
641EOF
642 } elsif ($virt ne "systemd-nspawn") {
643 $bootLoaderConfig = <<EOF;
644 # Use the GRUB 2 boot loader.
645 boot.loader.grub.enable = true;
646 boot.loader.grub.version = 2;
647 # boot.loader.grub.efiSupport = true;
648 # boot.loader.grub.efiInstallAsRemovable = true;
649 # boot.loader.efi.efiSysMountPoint = "/boot/efi";
650 # Define on which hard drive you want to install Grub.
651 # boot.loader.grub.device = "/dev/sda"; # or "nodev" for efi only
652EOF
653 }
654
655 my $networkingDhcpConfig = generateNetworkingDhcpConfig();
656
657 my $xserverConfig = generateXserverConfig();
658
659 (my $desktopConfiguration = <<EOF)=~s/^/ /gm;
660@desktopConfiguration@
661EOF
662
663 write_file($fn, <<EOF);
664@configuration@
665EOF
666 print STDERR "For more hardware-specific settings, see https://github.com/NixOS/nixos-hardware.\n"
667 } else {
668 print STDERR "warning: not overwriting existing $fn\n";
669 }
670}
671
672# workaround for a bug in substituteAll