#! /usr/bin/perl
# $OpenBSD: update-plist,v 1.195 2021/03/07 19:30:16 gnezdo Exp $
# Copyright (c) 2018 Marc Espie <espie@openbsd.org>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

use strict;
use warnings;

# XXX we could pass these as -D options, but it would mean a lot of the
# code would get run as root before the drop privileges, so checking the
# env for the fake identity is much safer!

my $ports1;
my ($ports_uid, $ports_gid, $fake_uid, $fake_gid);
BEGIN {
	my $ports = $ENV{PORTSDIR};
	$ports1 = $ports || '/usr/ports';
	# if we're root
	return if $< != 0;
	# switch id right away
	my $fake = $ENV{FAKE_TREE_OWNER};
	my $tree = $ENV{PORTS_TREE_OWNER};
	# XXX we can only end there if we're very naughty and building
	# everything as root, but not behind PORTS_PRIVSEP
	if (!defined $fake || !defined $tree || !defined $ports) {
		print STDERR "DON'T BUILD PORTS AS ROOT!!!!!\n";
		print STDERR "(or make sure you pass env variables PORTS_TREE_OWNER, FAKE_TREE_OWNER and PORTSDIR thru doas to root)\n";
		return;
	}
	die "FAKE_TREE_OWNER not defined" unless defined $fake;
	die "PORTS_TREE_OWNER not defined" unless defined $tree;

	($fake_uid, $fake_gid) = (getpwnam $fake)[2,3];
	($ports_uid, $ports_gid) = (getpwnam $tree)[2,3];
	die "User $fake not found" unless defined $fake_uid;
	die "User $tree not found" unless defined $ports_uid;
	$) = $fake_gid;
	$> = $fake_uid;
}
use lib "$ports1/infrastructure/lib";
use OpenBSD::FS2;
use OpenBSD::ReverseSubst;
use OpenBSD::CommonPlist;

package TrackedFile;
sub new
{
	my ($class, $name, $ext) = @_;
	bless {name => $name, ext => $ext, items => [], items2 => []}, $class;
}

sub add
{
	my ($self, $item) = @_;
	push(@{$self->{items}}, $item);
}

sub add2
{
	my ($self, $item, $p) = @_;

	if ($item->NoDuplicateNames) {
		my $s = $p->subst->remove_ignored_vars($item->{prepared});
		my $s2 = $p->subst->do($s);
		if (defined (my $k = $item->keyword)) {
			$s2 =~ s/^\@\Q$k\E\s//;
		}
		$p->{stash}{$s2}++;

		my $comment = $p->subst->{maybe_comment};

		if ($s ne $item->{prepared} &&
			$item->{prepared} !~ m/^\Q$comment\E/) {
			$item->{candidate_for_comment} = $s2;
		}
	}
	push(@{$self->{items2}}, $item);
}

sub fh
{
	my $self = shift;
	if (!defined $self->{fh}) {
		my $full = $self->name.$self->{ext};
		open($self->{fh}, '>', $full) or die "Can't open $full: $!";
	}
	return $self->{fh};
}

sub name
{
	my $self = shift;
	return $self->{name};
}

sub next_item
{
	my $self = shift;
	if (@{$self->{items}} != 0) {
		return shift @{$self->{items}};
	} else {
		return undef;
	}
}

sub next_item2
{
	my $self = shift;
	if (@{$self->{items2}} != 0) {
		return shift @{$self->{items2}};
	} else {
		return undef;
	}
}

package TrackFile;

sub new
{
	my ($class, $default, $ext) = @_;
	my $self = bless {ext => $ext}, $class;
	$self->{known}{$default} = 
	    $self->{default} = TrackedFile->new($default, $self->{ext});
	return $self;
}

sub file
{
	my ($self, $name) = @_;
	$self->{known}{$name} //= TrackedFile->new($name, $self->{ext});
	return $self->{known}{$name};
}

sub default
{
	my $self = shift;
	return $self->{default};
}

sub write_all
{
	my ($self, $p) = @_;

	for my $i (@{$p->{base_plists}}) {
		# we mimic the way pkg_create writes files
		$p->{restate} = {};

		my @stack = ();
		push(@stack, $self->file($i));


		while (my $file = pop @stack) {
			while (my $j = $file->next_item) {
				my $filename = $j->prepare_restate($file, $p);
				if (defined $filename) {
					push(@stack, $file);
					$file = $self->file($filename);
				}
			}
		}
	}

	for my $i (@{$p->{base_plists}}) {
		# we mimic the way pkg_create writes files
		$p->{restate} = {};

		my @stack = ();
		push(@stack, $self->file($i));


		while (my $file = pop @stack) {
			while (my $j = $file->next_item2) {
				my $filename = $j->write_restate($file, $p);
				if (defined $filename) {
					push(@stack, $file);
					$file = $self->file($filename);
				}
			}
			close($file->fh);
		}
	}
}

# PlistReader is "just" a specialized version of PkgCreate algorithm
# that does mimic what PkgCreate reader does with a few specialized methods
package PlistReader;
our @ISA = qw(OpenBSD::BasePlistReader);

use File::Path qw(make_path);
use File::Basename;

sub new
{
	my $class = shift;
	my $o = $class->SUPER::new;
	$o->{nlist} = OpenBSD::PackingList->new;
	return $o;
}

sub stateclass
{
	return 'PlistReader::State';
}

sub command_name
{
	return 'update-plist';
}

sub nlist
{
	my $self = shift;
	return $self->{nlist};
}

sub process_next_subpackage
{
	my ($class, $o) = @_;
	my $r = $class->SUPER::process_next_subpackage($o);

	$r->nlist->set_pkgname($r->olist->pkgname);
	# add the cwd to new list as well!!!
	OpenBSD::PackingElement::Cwd->add($r->nlist, $r->{state}{prefix});
	$r->add_extra_info($r->olist, $r->{state});
}

sub strip_prefix
{
	my ($self, $path) = @_;
	$path =~ s,^\Q$self->{state}{prefix}\E/,,;
	return $path;
}

sub subst
{
	my $self = shift;
	return $self->{state}{subst};
}

# this is where the magic happens, with the specialized methods
# e is the plist element
# self is the reader (with pkgname et al)
# file is the fileclass where this comes from
# unsubst is the full name before substitution
sub annotate
{
	my ($self, $e, $s, $file) = @_;
	$e->{file} = $file->name;
	$e->{comesfrom} = $self;

	return unless defined $s;
	chomp $s;
	$e->{unsubst} = $s;

	return unless $s =~ m/\$/o;	# optimization

	# so we redo what subst does, but we keep track of it!
	my $subst = $self->{state}{subst};
	while ( my $k = ($s =~ m/\$\{([A-Za-z_][^\}]*)\}/o)[0] ) {
		my $v = $subst->value($k);
		$subst->{used}{$k} = 1;
		unless ( defined $v ) { $v = "\$\\\{$k\}"; }
		$s =~ s/\$\{\Q$k\E\}/$v/g;
	}
}

# and more magic, we want to record fragments as pseudo-objects
sub record_fragment
{
	my ($self, $plist, $not, $name, $file) = @_;
	my $f;
	if ($not) {
		$f = OpenBSD::PackingElement::NoFragment->add($plist, $name);
	} else {
		$f = OpenBSD::PackingElement::Fragment->add($plist, $name);
	}
	$self->annotate($f, undef, $file);
}


# okay, so that plist doesn't exist, wouhou, I don't care,
# since I'm not pkg_create
sub cant_read_fragment
{
}

sub missing_fragments
{
}

# XXX we should go to the tree for self, always. Don't grab bad data from
# old packages or cache.  At the very least invalidate if the version number
# changes!
sub get_plist
{
	my ($self, $pkgpath, $portsdir) = @_;
	my $fullpath;
	if (defined $self->{state}{cache_dir}) {
		$fullpath = $pkgpath;
		# flatten the pkgpath proper
		$fullpath =~ s,/,.,g;
		$fullpath = "$self->{state}{cache_dir}/$fullpath";
		if (-f $fullpath) {
			return OpenBSD::PackingList->fromfile($fullpath,
			    \&OpenBSD::PackingList::UpdatePlistOnly);
		} else {
			make_path(dirname($fullpath));
		}
	}
	my $plist = OpenBSD::Dependencies::CreateSolver->ask_tree(
	    $self->{state}, $pkgpath, $portsdir,
	    \&OpenBSD::PackingList::UpdatePlistOnly,
	    "print-plist-with-depends", "wantlib_args=no-wantlib-args");
	if (defined $fullpath) {
		$plist->tofile($fullpath);
	}
	return $plist;
}

sub figure_out_dependencies
{
	my ($self, $cache, $portsdir) = @_;
	my @solve = ();
	my %solve = ();
	my $register = $self->{directory_register};
	# compute initial list of dependencies
	for my $full (keys %{$self->{state}{dependencies}}) {
		next unless $full =~ m/^(.*?):/;
		push(@solve, $1);
		$solve{$1} = 1;
	}

	# and do the walk
	while (@solve != 0) {
		# optimization: don't try if we don't have directories left
		return if !%$register;
		my $pkgpath = pop @solve;
		if (!defined $cache->{$pkgpath}) {
			$cache->{$pkgpath} = {};
			$self->{state}->say("Stripping directories from #1",
			    $pkgpath) unless $self->{state}{quiet};
			my $plist = $self->get_plist($pkgpath, $portsdir);
			$plist->process_dependency($cache->{$pkgpath});
		}
		for my $dir (keys %{$cache->{$pkgpath}{dir}}) {
			if (defined $register->{$dir}) {
				$register->{$dir}{DONT} = 1;
				$self->{stripped}{$dir} = $pkgpath;
				delete $register->{$dir};
			}
		}
		for my $k (keys %{$cache->{$pkgpath}{pkgpath}}) {
			push(@solve, $k) unless defined $solve{$k};
			$solve{$k} = 1;
		}
	}
}

# specialized state
package PlistReader::State;
our @ISA = qw(OpenBSD::BasePlistReader::State);

# our subst will record everything
sub substclass
{
	return 'OpenBSD::ReverseSubst';
}

# Most of the heavy lifting is done by visitor methods, as always

package OpenBSD::PackingElement;

use File::Basename;

# record everything we need to know about the object:
# exact file name, approximate directories, possible command names
# that must come after unexec
sub known_object
{
}

# record known directories and their parents as anchors for new objects
# note we can't mark directories for stripping yet, as we don't have them all
sub known_directory
{
}

# while scanning a dependency, note further dependencies to process,
# and directories we can strip
sub process_dependency
{
}

# keep a record of directories that can get removed by dependencies
sub tag_directories
{
}

# non-file objects that can be copied directly, as their location is automatic
# (e.g., conflict, pkgpath, etc)
sub copy_annotations
{
}

# the actual method that stores the objects for writing, dispatching them
# to the correct fragment
sub redistribute
{
	my ($o, $p) = @_;
	return if $o->{DONT};
	if (defined $o->{file}) {
		$p->{tracker}->file($o->{file})->add($o);
	} else {
		$p->{tracker}->default->add($o);
	}
}

# the actual method that keeps state (@mode/@owner/@group) and does
# backsubstitution and writing.
# part of the state is the current fragment, so it should return the
# new filename when it changes
# Note that this is not called as a visitor, but directly by the FileTracker
# on the lists it builds
sub write_restate
{
	my ($o, $file, $p) = @_;
	$o->write_backsubst($file, $p);
	return undef;
}

sub prepare_restate
{
	my ($o, $file, $p) = @_;
	$o->prepare_backsubst($file, $p);
	return undef;
}

sub prepare_backsubst
{
	my ($o, $file, $p) = @_;
	my $s = $p->subst->do_backsubst($o->fullstring, $o->unsubst, $o);
	$o->{prepared} = $s;
	$file->add2($o, $p);
}

# default backsubstitution and writing. 
sub write_backsubst
{
	my ($o, $file, $p) = @_;

	if (defined (my $s = $o->{candidate_for_comment})) {
		if ($p->{stash}{$s} > 1) {
			$o->{prepared} = 
			    $p->subst->{maybe_comment}.$o->{prepared};
		}
	}
	print {$file->fh} $o->{prepared}, "\n";
}

# extra objects that get copied very late (e.g., @extra)
sub copy_extra 
{
}


# some objects will have lists of tags, so that when they get copied
# the tags come with them
sub tag_along
{
	my ($self, $n) = @_;

	push(@{$self->{mytags}}, $n);
	$n->{tagged} = 1;
}

# this is the actual marking for later:
# we "keep state" of objects that accept tags (because they were found
# so we know we'll get them)
# and objects that are not found will tag along if appropriate
sub tie_objects
{
	my ($self, $plist) = @_;
	if ($self->{found}) {
		$self->bookmark($plist);
	} else {
		$self->may_tag_along($plist);
	}
}

# so this will use the default attach_to_lastobject mostly
sub attach_to_lastobject
{
	my ($self, $plist) = @_;
	if (defined $plist->{state}{lastobject}) {
		$plist->{state}{lastobject}->tag_along($self);
	}
}

# if the object is appropriate, it becomes a last object
sub bookmark
{
}

# if the object is appropriate, it will tag along
sub may_tag_along
{
}

# warn about files with a wrong name (.swp, ~, .orig)
# or fuss with paths
sub last_check
{
}

# record every cvstag in existence and files that will be written
# so that files without a cvstag will gain one
sub find_existing_cvstags
{
	my ($self, $filenames, $existing) = @_;
	if (defined $self->{file}) {
		$filenames->{$self->{file}} = 1;
	}
}

sub show_unknown
{
	my $self = shift;
	if (!$self->{found}) {
		print "Not found: ", $self->fullstring, " (in ", 
		    $self->{file}, ")\n";
	}
}

# this is not used as a visitor, but rather invoked explicitly when copying
# an object that can have tags
# TODO some tags should be copied "later" (in redistribute) so that they
# get in the plist "out-of-order" (comments in preamble)
sub copy_with_tags
{
	my ($self, $plist) = @_;

	$self->{found} = 1;
	$self->add_object($plist);
	if (defined $self->{mytags}) {
		for my $tag (@{$self->{mytags}}) {
			next if $tag->{found};
			$tag->{tagged_along} = 1;
			copy_with_tags($tag, $plist);
		}
	}
}

# pass every actual file to pkglocate to check for unregistered conflicts
sub locate_files
{
}

# will be zero for classes that cannot be deduced from the FS
sub rebless_okay { 1 }

# unexec should only match objects which are actual files and not directories
sub is_file { 0 }

# helper method
# the code that checks the suffixes
sub check_suffix
{
	my ($self, $state) = @_;
	my $s = $self->fullname;
	my $error;
	if ($s =~ m/\/\.[^\/]*\.swp$/) {
		$error = "vim swap file";
	} elsif ($s =~ m/\~$/) {
		$error = "emacs temporary file";
	} else {
		for my $suf (@{$state->{warn_suffix}}) {
			if ($s =~ m/\Q$suf\E$/) {
				$error = "$suf suffix";
				last;
			}
		}
	}
	return $error;
}

# helper method
# @extra and friends may have unneeded ${PREFIX} prepended to them
sub strip_redundant_absolute
{
	my ($self, $p) = @_;
	# remove unneeded absolute paths
	if ($self->name =~ m/^\// && $self->cwd eq $p->{state}{prefix}) {
		$self->{name} = $p->strip_prefix($self->name);
	}
}

sub unsubst
{
	my $a = shift;

	if (!defined $a->{unsubst} && defined $a->{hint_dir}) {
		my $d = $a->{hint_dir};
		my $o = $a->{hint_obj};

		# handle keywords
		my $s = $a->fullstring;
		my $subst = $o->{comesfrom}->subst;
		my $d2 = $o->{unsubst};
		my $k = '';
		if ($s =~ s/^(\@\S+\s+)//) {
			$k = $1;
		}
		$d2 =~ s/^(\@\S+\s+)//;
		# so figure out the maximum possible directory
		while (1) {
			my $s2 = $subst->do($d2);
			if ($s2 =~ m/\/$/) {
				if ($s =~ m/^\Q$s2\E/) {
					$a->{unsubst} = "$k$d2";
					last;
				}
		    	} else {
				if ($s =~ m/^\Q$s2\E\//) {
					$a->{unsubst} = "$k$d2/";
					last;

				}
			}
			last if $s2 eq '/' or $s2 eq '.';
			$d2 = dirname($d2);
		}
#		for debugging, commented out
#		print $a->fullstring, " gains partial $a->{unsubst} from ",
#		    $o->{unsubst}, "\n";
	}
	return $a->{unsubst};
}

sub rebless
{
	my ($self, $newclass) = @_;
	my $old_prefix = $self->fullstring;
	$old_prefix =~ s/^(\@\S+\s|).*/$1/;
	bless $self, $newclass;
	my $new_prefix = $self->fullstring;
	$new_prefix =~ s/^(\@\S+\s|).*/$1/;
	if (defined $self->unsubst) {
		$self->{unsubst} =~ s/^\Q$old_prefix\E/$new_prefix/;
	}
}

# check_specific($h): some file types have specific needs (e.g., @tag)
# so we record these as $h->{should}, and we record the actual
# @tag  if we see it as $h->{has}.
sub check_specific
{
}

# placeholder if ever we need to do something when SOME specific entries
# change
sub notice_new_file
{
}

# write anything that will affect pkglocate
sub write_conflict_info
{
	my ($self, $fh) = @_;
	if ($self->is_part_of_conflict_info) {
		$self->write($fh);
	}
}

sub is_part_of_conflict_info
{ 0 }

package OpenBSD::PackingElement::State;

# that stuff NEVER gets copied over, but interpolated from existing objects
sub show_unknown
{
}

package OpenBSD::PackingElement::Dependency;
sub process_dependency
{
	my ($self, $mtree) = @_;
	$mtree->{pkgpath}{$self->{pkgpath}} = 1;
}

package OpenBSD::PackingElement::DirlikeObject;
sub process_dependency
{
	my ($self, $mtree) = @_;

	$mtree->{dir}{$self->fullname} = 1;
}

sub tag_directories
{
	my ($self, $h) = @_;

	$h->{$self->fullname} = $self;
}

package OpenBSD::PackingElement::DirBase;
sub bookmark
{
	my ($self, $plist) = @_;
	$plist->{state}{lastobject} = $self;
	$plist->{state}{lastdir} = $self;
}

package OpenBSD::PackingElement::Meta;
sub copy_annotations
{
	my ($self, $plist) = @_;
	$self->{found} = 1;
	$self->clone->add_object($plist);
}

package OpenBSD::PackingElement::UniqueOption;

package OpenBSD::PackingElement::CVSTag;
sub copy_annotations
{
	my ($self, $plist) = @_;
	$self->copy_with_tags($plist);
}

sub find_existing_cvstags
{
	my ($self, $filenames, $existing) = @_;
	$existing->{$self->{file}} = 1;
	$self->SUPER::find_existing_cvstags($filenames, $existing);
}

sub tie_objects
{
	my ($self, $plist) = @_;
	$plist->{state}{lastobject} = $self;
}

# we will never do backsubst on CVSTags
sub prepare_backsubst
{
	my ($o, $file, $p) = @_;
	$o->{prepared} = $o->fullstring;
	$file->add2($o, $p);
}

# this is extra stuff that PkgCreate  builds but that we don't want to copy
package OpenBSD::PackingElement::Name;
sub copy_annotations
{
}

sub show_unknown
{
}

sub is_part_of_conflict_info
{ 1 }

package OpenBSD::PackingElement::NoDefaultConflict;
sub is_part_of_conflict_info
{ 1 }

package OpenBSD::PackingElement::Conflict;
sub is_part_of_conflict_info
{ 1 }

package OpenBSD::PackingElement::SpecialFile;
sub copy_annotations
{
}

sub show_unknown
{
}

sub write_restate
{
}

sub prepare_restate
{
}

package OpenBSD::PackingElement::ExtraInfo;
sub copy_annotations
{
}

sub show_unknown
{
}

package OpenBSD::PackingElement::Cwd;
sub show_unknown
{
}

package OpenBSD::PackingElement::Comment;
# comments need to pretend they're like file objects, so that you can comment
# file objects
sub fullname
{
	my $self = shift;
	my $path = $self->name;
	# strip every keyword for matching
	$path =~ s/^\@\w+\s+//;
	if ($path !~ m|^/|o && $self->cwd ne '.') {
		$path = $self->cwd."/".$path;
		$path =~ s,^//,/,;
	}
	$path =~ s,/$,,;
	return $path;
}

# comments that are not found as actual paths will tag along after the last
# object they saw
sub may_tag_along
{
	my ($self, $plist) = @_;
	if ($self->{name} =~ m/^intentional/i && 
	    defined $plist->{state}{lastobject}) {
		$plist->{state}{lastobject}{intentional} = 1;
	}
	$self->attach_to_lastobject($plist);
}

sub known_object
{
	&OpenBSD::PackingElement::FileObject::known_object;
}

sub cwd
{
	&OpenBSD::PackingElement::Object::cwd;
}

sub copy_annotations
{
	# nope these are not normal annotations we can copy
}

sub last_check
{
	my ($self, $p, $state) = @_;

	$self->strip_redundant_absolute($p);
	return if !defined $self->{tagged_along};
	my $error = $self->check_suffix($state);
	if (defined $error) {
		push(@{$p->{oldcomments}}, $self->fullstring. 
		    " (no matching file and $error ?)");
	}
}

# if a file was commented, do not bring it back as a real file
sub rebless_okay { 0 }

package OpenBSD::PackingElement::Sample;
sub may_tag_along
{
	my ($self, $plist) = @_;
	my $o = $self->{copyfrom};
	if (!defined $o) {
		print STDERR "Warning: bogus \@sample ", $self->stringize,
		    " with no reference file\n";
	} elsif (!$o->{found}) {
		print STDERR "Warning: ", $self->stringize,
		    " references a non-existing file ", $o->stringize, 
		    " and will not be copied\n";
	} else {
		$o->tag_along($self);
	}
}

sub known_object
{
	my ($self, $o) = @_;
	my $f = $self->fullname;
	push @{$o->{sample}{$f}}, $self;
}

package OpenBSD::PackingElement::Tag;
sub check_specific
{
	my ($self, $h) = @_;
	$h->{has}{$self->stringize} = 1;
}

package OpenBSD::PackingElement::Desktop;
our @ISA=qw(OpenBSD::PackingElement::File);
sub check_specific
{
	my ($self, $h) = @_;
	$h->{should}{'update-desktop-database'} = 1;
}

package OpenBSD::PackingElement::Glib2Schema;
our @ISA=qw(OpenBSD::PackingElement::File);
sub check_specific
{
	my ($self, $h) = @_;
	$h->{should}{'glib-compile-schemas'} = 1;
}

package OpenBSD::PackingElement::IconThemeDirectory;
our @ISA=qw(OpenBSD::PackingElement::Dir);
sub check_specific
{
	my ($self, $h) = @_;
	$h->{should}{"gtk-update-icon-cache %D/".$self->name} = 1;
}

package OpenBSD::PackingElement::IconTheme;
our @ISA=qw(OpenBSD::PackingElement::File);
use File::Basename;

sub check_specific
{
	my ($self, $h) = @_;
	# XXX this works because the file happens *after* its parent directory
	delete $h->{should}{"gtk-update-icon-cache %D/".dirname($self->name)};
}

package OpenBSD::PackingElement::MimeInfo;
our @ISA=qw(OpenBSD::PackingElement::File);
sub check_specific
{
	my ($self, $h) = @_;
	$h->{should}{'update-mime-database'} = 1;
}

package OpenBSD::PackingElement::Sampledir;

# this is not really smart, but good enough for starters
sub may_tag_along
{
	my ($self, $plist) = @_;
	$self->attach_to_lastobject($plist);
}

# likewise, sampledirs do not want to become normal dirs
sub rebless_okay { 0 }

# those are objects that only exist in update-plist
package OpenBSD::PackingElement::Fragment;
our @ISA=qw(OpenBSD::PackingElement);

{
no warnings qw(redefine);
sub needs_keyword() { 0 }

sub stringize
{
	return '%%'.shift->{name}.'%%';
}
}

# copy them in the right location
sub may_tag_along
{
	my ($self, $plist) = @_;
	$self->attach_to_lastobject($plist);
}

# while writing, change file accordingly
sub write_restate
{
	my ($self, $file, $p) = @_;
	# don't do backsubst on fragments, pkg_create does not!
	$self->write($file->fh);
	my $base = $file->name;
	my $frag = $self->frag;
	$base =~ s/PFRAG\./PFRAG.$frag-/ or
	    $base =~ s/PLIST/PFRAG.$frag/;
	return $base if $p->{tracker}{known}{$base};
	return undef;
}

sub prepare_restate
{
	my ($self, $file, $p) = @_;
	# don't do backsubst on fragments, pkg_create does not!
	$file->add2($self, $p);
	my $base = $file->name;
	my $frag = $self->frag;
	$base =~ s/PFRAG\./PFRAG.$frag-/ or
	    $base =~ s/PLIST/PFRAG.$frag/;
	return $base if $p->{tracker}{known}{$base};
	return undef;
}

sub frag
{
	my $self = shift;
	return $self->{name};
}

package OpenBSD::PackingElement::NoFragment;
our @ISA=qw(OpenBSD::PackingElement::Fragment);
{
no warnings qw(redefine);
sub stringize
{
	return '!%%'.shift->{name}.'%%';
}
}

sub frag
{
	my $self = shift;
	return "no-$self->{name}";
}

package OpenBSD::PackingElement::Action;

# TODO old make-plist would check whether the substitutions didn't change
sub may_tag_along
{
	my ($self, $plist) = @_;
	# for now, we might do something smarter later
	$self->attach_to_lastobject($plist);
}

package OpenBSD::PackingElement::Unexec;
sub known_object
{
	my ($self, $o) = @_;
	# figure out possible commands in the list
	for my $i (split/\s+/, $self->{expanded}) {
		next if $i eq "/usr/bin/env";
		next if $i =~ m/^\-/;
		next if $i =~ m/\=/;
		$o->{comes_after}{$i} = $self;
	}
}

package OpenBSD::PackingElement::FileObject;
use File::Basename;

# FileObjects are (mostly) stuff with paths that can get substs...
sub last_check
{
	my ($self, $p, $state) = @_;

	$self->strip_redundant_absolute($p);
	return if $self->{intentional};
	my $error = $self->check_suffix($state);
	return unless defined $error;
	if (defined $self->{comesfrom}) {
		push(@{$p->{oldorigfiles}}, $self->fullstring. " ($error ?)");
	} else {
		$self->{DONT} = 1;
		push(@{$p->{origfiles}}, $self->fullstring. " ($error ?)");
	}
}

sub known_object
{
	my ($self, $o) = @_;
	my $f = $self->fullname;
	push @{$o->{exact}{$f}}, $self;
	delete $o->{approximate}{$f};
}

sub known_directory
{
	my ($self, $o, $plist) = @_;
	my $d = $self->fullname;
	while (1) {
		$d = dirname($d);
		# don't go up to / if we can avoid it
		return if $d eq $self->cwd or $d eq '/';
		return if defined $self->{$d}{$plist};
		$o->{approximate}{$d}{$plist} = $self;
	}
}

sub show_unknown
{
	my $self = shift;
	if (!$self->{found}) {
		print "Not found: ", $self->fullname, " (in ", $self->{file}, ")\n";
	}
}

sub write_restate
{
	my ($self, $f, $p) = @_;
	
	# TODO there should be some more code matching the mode to the original
	# file that was copied
	for my $k (qw(mode owner group)) {
		my $s = "\@$k";
		if (defined $self->{$k}) {
			if (defined $p->{restate}{$k}) {
				if ($p->{restate}{$k} eq $self->{$k}) {
					next;
				}
			}
			if ($k eq 'mode') {
				$s .= " ".$self->{$k};
			} else {
				$s .= " ".
				    $p->subst->do_backsubst($self->{$k}, undef);
			}
		} else {
			if (!defined $p->{restate}{$k}) {
				next;
			}
		}
		$p->{restate}{$k} = $self->{$k};
		print {$f->fh} $s, "\n";
	}
	$self->write_backsubst($f, $p);
	return undef;
}

package OpenBSD::PackingElement::FileBase;
sub bookmark
{
	my ($self, $plist) = @_;
	$plist->{state}{lastobject} = $self;
	$plist->{state}{lastfile} = $self;
}

sub locate_files
{
	my ($self, $locator, $exact) = @_;
	my $p = $self->fullname;
	if (!exists $exact->{$p}) {
		$locator->add_param($p);
	}
}

sub write_backsubst
{
	my ($self, $f, $p) = @_;
	if (defined $self->{nochecksum}) {
		print {$f->fh} "\@comment no checksum\n";
	}
	if (defined $self->{nodebug}) {
		print {$f->fh} "\@comment no debug\n";
	}
	$self->SUPER::write_backsubst($f, $p);
}

sub is_file { 1 }

package OpenBSD::PackingElement::Shell;
# we have no way to figure out @shell
sub rebless_okay { 0 }

package OpenBSD::PackingElement::Lib;
my $first_warn = 1;
sub check_lib_version
{
	my ($self, $version, $name, $v) = @_;
	if (defined $v) {
		return if $v eq $version;
		print STDERR "ERROR: version mismatch for lib: ", $name,
		    " (", $version, " vs. ", $v, ")\n";
	} else {
		if ($first_warn) {
			print STDERR "Warning: unregistered shared lib(s)\n";
			$first_warn = 0;
		}
		print STDERR "SHARED_LIBS +=\t$name ",
		    ' 'x (25-length $name), "0.0 # $version\n";
	}
}

sub prepare_backsubst
{
	my ($self, $f, $p) = @_;
	if ($self->name =~ m,^(.*?)lib([^\/]+)\.so\.(\d+\.\d+)$,) {
		my ($path, $name, $version) = ($1, $2, $3);
		my $k = "LIB${name}_VERSION";
		# XXX redo backsubst on the variable name
		my $s = $p->subst->do_backsubst(
		    "\@lib ${path}lib$name.so.\$\{$k\}", $self->unsubst, $self);
		$self->check_lib_version($version, $name, 
			$p->subst->value($k));
		$self->{prepared} = $s;
		$f->add2($self, $p);
	} else {
		$self->SUPER::write_backsubst($f, $p);
	}
}

package OpenBSD::PackingElement::Extra;
sub copy_extra
{
	my ($self, $plist) = @_;

	if (!$self->{found}) {
		$self->{found} = 1;
		$self->clone->add_object($plist);
	}
}

sub may_tag_along
{
	my ($self, $plist) = @_;
	$self->attach_to_lastobject($plist);
}

sub rebless_okay() { 0 }

package OpenBSD::PackingElement::Extradir;
sub rebless_okay() { 0 }

sub copy_extra
{
	&OpenBSD::PackingElement::Extra::copy_extra;
}

package OpenBSD::PackingElement::Manpage;

sub check_suffix
{
	my ($self, $state) = @_;
	my $s = $self->fullname;
	my $error;
	if ($s =~ m/(\.Z|\.gz)$/) {
		$error = "compressed manpage";
	} elsif ($s =~ m/\.0$/) {
		$error = "preformatted manpage (USE_GROFF ?)";
	} elsif ($s =~ m/\.tbl$/) {
		$error = "unformatted .tbl manpage";
	}
	return $error;
}

# small class that runs pkglocate in batches
package OpenBSD::Pkglocate;
sub new
{
	my ($class, $state) = @_;
	my $ncpu;
	if (defined $state->opt('j')) {
		$ncpu = $state->opt('j');
	} else {
		$ncpu = `sysctl -n hw.ncpuonline`;
		chomp $ncpu;
	}
	bless {result => {}, params => [], bypath => {}, queue => [],
		ncpu => $ncpu}, $class;
}

sub add_param
{
	my ($self, @p) = @_;
	push(@{$self->{params}}, @p);
	while (@{$self->{params}} > 200) {
		$self->run_command;
	}
}

sub run_command
{
	my $self = shift;

	if (@{$self->{params}} == 0) {
		return;
	}
	if (@{$self->{queue}} > $self->{ncpu}) {
		$self->get_results;
	}
	my %h = map {($_, 1)} @{$self->{params}};
	# XXX so this is slightly tricky, we do run a pipe, and don't
	# look at the results just yet.
	# *if* the pipe produces lots of results, it will be stuck,
	# and when we grab the results, we will get stuff sequentially
	# but we are gambling that pipes produce few results each,
	# so they will just sit in the memory buffer when done
	# (we could also move to non-blocking pipes, which is slightly
	# crazy for such a small optimization)
	open(my $cmd, '-|', 'pkg_locate', map {":$_"} @{$self->{params}});
	push(@{$self->{queue}}, { h => \%h, pipe => $cmd});
	$self->{params} = [];
}

sub get_results
{
	my $self = shift;
	my $e = shift @{$self->{queue}};
	my $fh = $e->{pipe};
	while (<$fh>) {
		chomp;
		my ($pkgname, $pkgpath, $path) = split(':', $_, 3);

		# pkglocate will return partial results, we only care about
		# exact stuff
		if ($e->{h}{$path}) {
			push(@{$self->{result}{$pkgname}}, $path);
			$self->{bypath}{$pkgname} = $pkgpath;
		}
	}
	close($fh);
}

sub result
{
	my $self = shift;
	while (@{$self->{params}} > 0) {
		$self->run_command;
	}
	while (@{$self->{queue}}) {
		$self->get_results;
	}
	return $self->{result};
}

sub bypath
{
	my ($self, $pkgname) = @_;
	return $self->{bypath}{$pkgname};
}

# This is the UpdatePlist main code proper
package UpdatePlist::State;
our @ISA = qw(OpenBSD::AddCreateDelete::State);
sub handle_options
{
	my $state = shift;
	$state->{opt} = {
		'X' => sub {
			my $path = shift;
			$state->{ignored}{$path} = 1;
		    },
		'w' => sub {
			my $warn = shift;
			push(@{$state->{warn_suffix}}, $warn);
		    },
		'i' => sub {
			my $var = shift;
			push(@{$state->{dont_backsubst}}, $var);
		    },
		'I' => sub {
			my $var = shift;
			push(@{$state->{maybe_ignored}}, $var);
		    },
		'c' => sub {
			my $var = shift;
			if (exists $state->{maybe_comment}) {
				$state->usage;
			}
			$state->{maybe_comment} = '${'.$var.'}';
		    },
		's' => sub {
			my $var = shift;
			push(@{$state->{start_only}}, $var);
		    },
		'S' => sub {
			my $var = shift;
			push(@{$state->{suffix_only}}, $var);
		    },
		'V' => sub {
			my $var = shift;
			push(@{$state->{no_version}}, $var);
		    },

	};
	$state->SUPER::handle_options('rvI:c:qV:fFC:i:j:L:s:S:X:P:w:e:E:', 
	    '[-Ffmnrvx] [-C dir] [-c comment] [-E ext] [-e ext] [-i var]',
	    '[-I ignored] [-j jobs] [-L logfile] [-P pkgdir] [-S var]',
	    '[-s var] [-V var] [-w suffix] [-X path] -- pkg_create_args ...');
    	$state->{pkgdir} = $state->opt('P');
	$state->{scan_as_root} = $state->opt('r');
	$state->{verbose} = $state->opt('v');
	$state->{cache_dir} = $state->opt('C');
	$state->{quiet} = $state->opt('q');
	$state->{extnew} = $state->opt('E') // ".new";
	$state->{extorig} = $state->opt('e') // ".orig";
	$state->{logfile} = $state->opt('L');
	for my $i (qw(FAKE_COOKIE PKGLOCATE_COOKIE)) {
		$state->{$i} = $state->defines($i);
		if (defined $state->{$i}) {
			$state->{ignored}{$state->{$i}} = 1;
		}
	}
	if (exists $state->{maybe_ignored} && !exists $state->{maybe_comment}) {
		$state->usage;
	}
}

package UpdatePlist;
use File::Basename;
use File::Compare;

sub new
{
	my $class = shift;
	bless {
	    state => UpdatePlist::State->new,
	}, $class;
}

sub known_objects
{
	# let's record where each object live, including directory
	# locations.  As a rule, "exact" information will supersede 
	# deduced directory names.
	my $self = shift;
	for my $p (@{$self->{lists}}) {
		$p->olist->known_directory($self, $p->olist);
	}
	for my $p (@{$self->{lists}}) {
		$p->olist->known_object($self);
	}
}

sub scan_fake_dir
{
	my $self = shift;
	# XXX we assume all subpackage are under the same destdir (-B option)
	my $base = $self->{lists}[0]->{state}{base};

	# now we ask the file system what exists, and fill file 
	# objects according to that.
	$self->{state}->say("Scanning #1", $base)
	    unless $self->{state}{quiet};
	local $> = 0 if $self->{state}{scan_as_root};

	$self->{objects} = OpenBSD::FS2->fill($base, $self->{state}{ignored},
	    $self->{state}{logfile}, $self->{state});
}

sub zap_debug_files
{
	my $self = shift;
	$self->{state}->say("Removing .debug artefacts");
	my $keep = {};	# hash of directories to keep
	for my $path (keys %{$self->{objects}}) {
		next unless $path =~ m,(.*)\/\.debug\/,;
		my $dir = $1;
		if ($path =~ m,\/([^\/]+)\.dbg$, or
		    $path =~ m,\/([^\/]+\.a)$,) {
			    my $p2 = "$dir/$1";
			    my $o = $self->{objects}{$p2};
			    if (defined $o && $o->can_have_debug) {
				    delete $self->{objects}{$path};
				    next;
			    }
		}
		$keep->{$dir} = 1;
	}
	for my $path (keys %{$self->{objects}}) {
		next unless $path =~ m,(.*)\/\.debug$,;
		next if $keep->{path};
		if ($self->{objects}{$path}->is_dir) {
			delete $self->{objects}{$path};
		}
	}
}

sub add_missing_cvstags
{
	my ($list, $base) = @_;
	my $filenames = {};
	$filenames->{$base} = 1;
	my $existing = {};
	$list->find_existing_cvstags($filenames, $existing);
	for my $name (keys %$filenames) {
		next if $existing->{$name};
		my $o = OpenBSD::PackingElement::CVSTag->add($list, 
		    '$'.'OpenBSD: '.basename($name).',v$');
		$o->{file} = $name;
	}
}

sub copy_from_old
{
	my ($self, $e, $o, $unexec) = @_;

	my $s = $e->{comesfrom};
	if ($o->element_class ne ref($e) && $e->rebless_okay) {
		$e->rebless($o->element_class);
		$e->notice_new_file($self);
	}

	# mark it for later (see add_delayed_objects)
	if ($e->cwd ne $s->{state}{prefix}) {
		push(@{$s->{badcwd}}, $e);
		return;
	}

	if (defined $unexec && $e->is_file) {
		# XXX we need to unmark it so it can tag along
		delete $e->{found};
		$unexec->tag_along($e);
	} else {
		$e->copy_with_tags($s->nlist);
	}
}

sub copy_existing
{
	my ($self, $path, $o) = @_;

	if ($self->{exact}{$path}) {
		# this will be re-added to multiple paths if there are
		# multiple matching plists
		for my $e (@{$self->{exact}{$path}}) {
			$self->copy_from_old($e, $o, $self->{comes_after}{$path});
		}
		return 1;
	} else {
		return 0;
	}
}

sub handle_annotations
{
	my $self = shift;
	# First we figure out which objects will get copied.
	$self->{state}->say("Figuring out tie points")
	    unless $self->{state}{quiet};
	for my $path (keys %{$self->{objects}}) {
		my $o = $self->{objects}{$path};
		if ($self->{exact}{$path}) {
			for my $e (@{$self->{exact}{$path}}) {
				$e->{found} = 1;
			}
		}
	}

	# THEN we attach annotations to the closest known object
	# This is sturdy when files vanish, as we tag along with
	# the nearest file
	$self->{state}->say("Tieing loose objects")
	    unless $self->{state}{quiet};
	for my $p (@{$self->{lists}}) {
		$p->olist->tie_objects($p->olist);
		$p->olist->copy_annotations($p->nlist);
	}
}

sub walk_up_directory
{
	my ($self, $path, $c) = @_;
	# we didn't find it so we must create a new one
	# go up dir until we find a matching approximate dir
	my $d = $path;
	while (1) {
		$d = dirname($d);
		last if $d eq '/';
		next unless exists $self->{approximate}{$d};
		my @l = values %{$self->{approximate}{$d}};
		# if we do, we only write non ambiguous names
		if (@l == 1) {
			my $s = $l[0]->{comesfrom};
			my $p2 = $s->strip_prefix($path);
			if ($p2 =~ m/^\// && !$c->absolute_okay) {
				# this will get caught as new element
				# TODO list of data to build inside
				# its own cwd
				last;
			}
			my $a = $c->add($s->nlist, $p2);
			$a->notice_new_file($self);
			# and match the file
			$a->{file} = $s->{file};
			# unsubst is used as a hint in reversesubst, so we
			# can use the directory part BUT we need to figure
			# it out.  Delay it until we need it
			$a->{hint_dir} = $d;
			$a->{hint_obj} = $l[0];
			return 1;
		}
	}
	return 0;
}

sub last_resort
{
	my ($self, $path, $c) = @_;
	# try all lists in order, until we find one with 
	# the right prefix
	for my $p (@{$self->{lists}}) {
		my $p2 = $p->strip_prefix($path);
		if ($p2 =~ m|^/| && !$c->absolute_okay) {
			next;
		}
		my $a = $c->add($p->nlist, $p2);
		# and match the file
		$a->{file} = $p->{file};
		return 1;
		last;
	}
	return 0;
}

# somewhat devious: that file was created by the fake install, BUT we
# install a sample instead. Make sure the sample is copied over in some
# plist, though
sub is_a_sample
{
	my ($self, $path) = @_;
	return 0 unless defined $self->{sample}{$path};
	for my $e (@{$self->{sample}{$path}}) {
		return 1 if $e->{tagged};
	}
	return 0;
}

sub copy_object
{
	my ($self, $path) = @_;
	my $o = $self->{objects}{$path};

	return if $self->copy_existing($path, $o);
	my $c = $o->element_class;

	return if $self->walk_up_directory($path, $c);
	return if $self->last_resort($path, $c);

	return if $self->is_a_sample($path);
	# TODO this is where we should figure @cwd stuff
	# though it's generally better to have distinct plists
	# for several prefixes
	push(@{$self->{orphan_paths}}, $path);
}

sub copy_objects
{
	my $self = shift;
	$self->{state}->say("Copying objects")
	    unless $self->{state}{quiet};
	for my $path (sort keys %{$self->{objects}}) {
	    $self->copy_object($path);
	}
}

sub add_delayed_objects
{
	my $self = shift;
	# now we can handle stuff outside of cwd, if that applies
	for my $p (@{$self->{lists}}) {
		my $cwd = $p->{state}{prefix};
		# we destate the cwd to try to minimize dir changes
		# note that these items are sorted, so it won't switch
		# all over the place.
		for my $e (@{$p->{badcwd}}) {
			if ($e->cwd ne $cwd) {
				$cwd = $e->cwd;
				OpenBSD::PackingElement::Cwd->add($p->nlist, 
				    $cwd);
			}
			$e->copy_with_tags($p->nlist);
		}
	}
}


sub strip_dependency_directories
{
	my $self = shift;

	# so we read everything, let's figure out common directories
	my $cache = {};
	my $portsdir = $ENV{PORTSDIR};
	for my $p (@{$self->{lists}}) {
		$p->{directory_register} = {};
		$p->nlist->tag_directories($p->{directory_register});
	}

	for my $p (@{$self->{lists}}) {
		if (%{$p->{directory_register}}) {
			$p->figure_out_dependencies($cache, $portsdir);
		}
	}

	# replace the cache entries from disk with cache entries from new plists
	for my $p (@{$self->{lists}}) {
		my $pkgpath = $p->olist->fullpkgpath;
		# optimisation: it's not a dependency, so we don't care
		next if !defined $cache->{$pkgpath};
		$cache->{$pkgpath} = {};
		$self->{state}->say("Stripping directories from #1 (trying harder)", 
		    $pkgpath) unless $self->{state}{quiet};
		$p->nlist->process_dependency($cache->{$pkgpath});
	}

	# and redo the zapping all over again, now that we fudged the cache
	for my $p (@{$self->{lists}}) {
		if (%{$p->{directory_register}}) {
			$p->figure_out_dependencies($cache, $portsdir);
		}
	}
}

sub add_missing_tags
{
	my $self = shift;
	for my $p (@{$self->{lists}}) {
		my $h = { should => {}, has => {}};
		$p->nlist->check_specific($h);
		for my $k (keys %{$h->{should}}) {
			next if $h->{has}{$k};
			OpenBSD::PackingElement::Tag->add($p->nlist, $k);
		}
	}
}

sub adjust_final
{
	my $self = shift;
	for my $p (@{$self->{lists}}) {
		$p->nlist->{name}{DONT} = 1;
		# CWD that we added manually... this sucks a bit!!!
		$p->nlist->{items}[0]{DONT} = 1;
		$p->olist->copy_extra($p->nlist);
		for my $i (@{$p->{base_plists}}) {
			add_missing_cvstags($p->nlist, $i);
		}
		$p->nlist->last_check($p, $self->{state});
    	}
}

sub report_per_list
{
	my ($self, $key, $msg) = @_;
	for my $p (@{$self->{lists}}) {
		next unless exists $p->{$key};
		$self->{state}->say($msg, $p->nlist->pkgname);
		for my $e (@{$p->{$key}}) {
			$self->{state}->say(" #1", $e);
		}
	}
}

sub report_issues
{
	my $self = shift;
	if (exists $self->{orphan_paths}) {
		print "Can't put into any plist (no applicable prefix):\n";
		for my $p (@{$self->{orphan_paths}}) {
			print "\t$p\n";
		}
	}
	# let's show a quick summary of stuff we couldn't figure out
	for my $p (@{$self->{lists}}) {
		$p->olist->show_unknown;
	}
	$self->report_per_list("origfiles", 
	    "Warning: entries NOT added to #1:");
	$self->report_per_list("oldorigfiles", 
	    "Warning: possible problematic entries in #1:");
	$self->report_per_list("oldcomments", "Warning: #1 still contains:");
}

sub write_new_files
{
	my $self = shift;
	for my $p (@{$self->{lists}}) {
		# default is the last content we have, thanks ruby :(
		$p->{tracker} = TrackFile->new($p->{base_plists}->[-1],
		    $self->{state}{extnew});
		$p->nlist->redistribute($p);
		$p->{tracker}->write_all($p);
		# TODO   old make-plist noticed libraries with a 
		# LIB*_VERSION but no matching file
	}
}

sub display_stripped_info
{
	my $self = shift;
	return unless $self->{state}{verbose};
	for my $p (@{$self->{lists}}) {
		next unless exists $p->{stripped};
		print "Directories stripped from ", $p->nlist->pkgname, ":\n";
		for my $d (sort keys %{$p->{stripped}}) {
			print " ", $p->strip_prefix($d), 
			    " ($p->{stripped}{$d})\n";
		}
	}
}

sub short_list
{
	my ($self, $l) = @_;
	if (@$l > 10) {
		return join(' ', splice(@$l, 0, 10))."...";
	} else {
		return join(' ', @$l);
	}
}

sub locate_list
{
	my ($self, $p) = @_;
	my $locator = OpenBSD::Pkglocate->new($p->{state});
	my $exact = $self->{exact};	# by default do not look up known files
	if ($self->{state}->opt('f')) {	# unless we ask for them: neuter lookup
		$exact = {};		# table
	}
	$p->nlist->locate_files($locator, $exact);
	my $l = $p->nlist->conflict_list;
	my $r = $locator->result;
	for my $pkgname (sort keys %$r) {
		next if $l->conflicts_with($pkgname);
		my $path = $locator->bypath($pkgname);
		my $portsdir = $ENV{PORTSDIR};
		my $plist = OpenBSD::Dependencies::CreateSolver->ask_tree(
		    $self->{state}, $path, $portsdir, 
		    \&OpenBSD::PackingList::ConflictOnly, 
		    "print-plist",
		    # XXX pkglocate does not include default flavors
		    "FULLPATH=No");
		my $myname = $p->nlist->pkgname;
		# Cheat in case ask_tree didn't work anyhow.
		if (!defined $plist->pkgname) {
			$plist->set_pkgname($pkgname);
		}
		next if $plist->conflict_list->conflicts_with($myname);
		print "Warning: ", $myname, " conflicts with ", $pkgname, " (",
		    $path, "):", $self->short_list($r->{$pkgname}), "\n";
	}
}

sub no_need_to_run
{
	my ($self, $state) = @_;
	if (!defined $state->{PKGLOCATE_COOKIE} || 
	    !defined $state->{FAKE_COOKIE}) {
		return 0;
	}
	my $cookie;
	open my $fh, '>', \$cookie;
	for my $l (@{$self->{lists}}) {
		$l->nlist->write_conflict_info($fh);
	}
	close $fh;
	$state->{cookie} = $cookie;
	if (-e $state->{PKGLOCATE_COOKIE}) {
		# verify the cookie is more recent than fake
		if (!-e $state->{FAKE_COOKIE}) {
			return 0;
		}
		my $ts1 = (stat $state->{PKGLOCATE_COOKIE})[9];
		my $ts2 = (stat $state->{FAKE_COOKIE})[9];
		if ($ts1 < $ts2) {
			return 0;
		}
		# check whether conflict info changed
		open my $fh, '<', $state->{PKGLOCATE_COOKIE} or return 0;
		local $/;
		my $cookie = <$fh>;
		if ($cookie eq $state->{cookie}) {
			return 1;
		}
	}
	return 0;
}

sub write_cookie
{
	my ($self, $state) = @_;
	if (defined $state->{PKGLOCATE_COOKIE}) {
		open(my $cookie, '>', $state->{PKGLOCATE_COOKIE}) or return;
		print $cookie $state->{cookie};
	}
}

sub try_pkglocate
{
	my $self = shift;
	my $state = $self->{state};
	# hardcode the location for now
	if (-x '/usr/local/bin/pkg_locate') {
		if ($self->no_need_to_run($state)) {
			$state->say("pkglocate already ran")
			    unless $state->{quiet};
			return;
		}
		$state->say("Looking for unregistered conflicts")
		    unless $state->{quiet};
		for my $p (@{$self->{lists}}) {
			$self->locate_list($p);
		}
		$self->write_cookie($state);
    	} else {
		$state->say("Can't look for conflicts, pkglocatedb not installed");
	}
}

my $self = UpdatePlist->new;
PlistReader->parse_args($self);
$self->known_objects;
$self->scan_fake_dir;
$self->zap_debug_files;
$self->handle_annotations;
$self->copy_objects;
$self->add_missing_tags;

# XXX check the order of delayed objects (cwd) vs extra files (no actual cwd)?
$self->add_delayed_objects;
$self->strip_dependency_directories;
$self->adjust_final;

# TODO we should try to match new items (with no unsubst) to the closest
# directory with unsubst material, so that we get better hints at substitution



$self->display_stripped_info;
$self->report_issues;
if (!$self->{state}->opt('F')) {
	$self->try_pkglocate;
}

# switch to ports owner
if (defined $ports_gid) {
	$> = 0;
	$) = $ports_gid;
	$> = $ports_uid;
}

# this is the step responsible for adjusting mode AND backsubstituting
# variables!
$self->write_new_files;

# and now, we figure out where to move the new files
my @towrite = ();
my $cantmove = 0;

my $exitcode = 0;

my $new = $self->{state}{extnew};
my $orig = $self->{state}{extorig};
# let's see if we want to update things
for my $p (@{$self->{lists}}) {
	for my $k (sort keys %{$p->{tracker}{known}}) {
		if (-f $k) {
			if (!-f "$k$new") {
				print STDERR "No $k$new written\n";
				$exitcode = 1;
			# TODO get common code out of register-plist
			# to figure out what discrepancies don't really matter
			} elsif (compare($k, "$k$new") == 0) {
				unlink("$k$new") unless $self->{state}->not;
			} else {
				print "$k changed";
				push(@towrite, $k);
				if (-f "$k$orig") {
					print " but $k$orig exists\n";
					$cantmove = 1;
				} else {
					print "\n";
				}
			}
		} else {
			print "$k is new\n";
			push(@towrite, $k);
		}
	}
}

if ($cantmove) {
	exit(2);
}

if ($self->{state}->not) {
	exit($exitcode);
}

for my $k (@towrite) {
	if (-f $k) {
		rename($k, "$k$orig") or 
		    die "can't rename $k to $k$orig: $!";
	}
	rename("$k$new", $k) or 
		    die "can't rename $k$new to $k: $!";
}

exit($exitcode);
