Skip to content

Commit 40ce17b

Browse files
committed
Extract minimal set of lima and qemu files to install on a clean machine
Installing qemu via homebrew installs 846MB if stuff under /usr/local/Cellar, 571MB of which are the qemu cellar alone. This is clearly excessive for bundling lime with an app that simply wants to deploy VMs. The attached script uses dtrace to observe which files are actually opened by lima and qemu, and then collects just those. It collects the corresponding symlinks and also modifies all Mach-O binaries to reference dylibs relative to the path of the executable, so the files can be relocated anywhere. There are still hard-coded paths to things under /user/local inside the binaries, but testing shows that lima works with the bundled files as-is. Signed-off-by: Jan Dubois <[email protected]>
1 parent 5d337b9 commit 40ce17b

File tree

1 file changed

+155
-0
lines changed

1 file changed

+155
-0
lines changed

hack/lima-and-qemu.pl

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#!/usr/bin/env perl
2+
use strict;
3+
use warnings;
4+
5+
use FindBin qw();
6+
7+
# By default capture both legacy firmware (alpine) and UFI (default) usage
8+
@ARGV = qw(alpine default) unless @ARGV;
9+
10+
# This script creates a tarball containing lima and qemu, plus all their
11+
# dependencies from /usr/local/**.
12+
#
13+
# New processes (with their command line arguments) have been captured by
14+
# `sudo dtrace -s /usr/bin/newproc.d` (on a system with SIP disabled, using lima 0.3.0):
15+
# `limactl start examples/alpine.yaml; limactl stop alpine; limactrl delete alpine`.
16+
#
17+
# 5680 <777> limactl start --tty=false examples/alpine.yaml
18+
# 5681 <5680> curl -fSL -o /Users/jan/Library/Caches/lima/download/by-url-sha256/21753<...>
19+
# 5683 <5680> qemu-img create -f qcow2 /Users/jan/.lima/alpine/diffdisk 107374182400
20+
# 5684 <5680> /usr/local/bin/limactl hostagent --pidfile /Users/jan/.lima/alpine/ha.pid alpine
21+
# 5686 <5684> ssh-keygen -R [127.0.0.1]:60020 -R [localhost]:60020
22+
# 5687 <5684> ssh -o ControlMaster=auto -o ControlPath=/Users/jan/.lima/alpine/ssh.sock -o <...>
23+
# 5685 <5684> /usr/local/bin/qemu-system-x86_64 -cpu Haswell-v4 -machine q35,accel=hvf -smp <...>
24+
# 5689 <5684> ssh -o ControlMaster=auto -o ControlPath=/Users/jan/.lima/alpine/ssh.sock -o <...>
25+
# ... many more ssh sub-processes like the one above ...
26+
# 5800 <777> limactl stop alpine
27+
# 5801 <5684> ssh -o ControlMaster=auto -o ControlPath=/Users/jan/.lima/alpine/ssh.sock -o <...>
28+
# 5896 <777> limactl delete alpine
29+
#
30+
# It shows the following binaries from /usr/local are called:
31+
32+
my $install_dir = "/usr/local";
33+
record("$install_dir/bin/limactl");
34+
record("$install_dir/bin/qemu-img");
35+
record("$install_dir/bin/qemu-system-x86_64");
36+
37+
# Capture any library and datafiles access with opensnoop
38+
my $opensnoop = "/tmp/opensnoop.log";
39+
END { system("sudo pkill dtrace") }
40+
print "sudo may prompt for password to run opensnoop\n";
41+
system("sudo -b opensnoop >$opensnoop 2>/dev/null");
42+
sleep(1) until -s $opensnoop;
43+
44+
my $repo_root = dirname($FindBin::Bin);
45+
for my $example (@ARGV) {
46+
my $config = "$repo_root/examples/$example.yaml", ;
47+
die "Config $config not found" unless -f $config;
48+
system("limactl delete -f $example") if -f "$ENV{HOME}/.lima/$example";
49+
system("limactl start --tty=false $config");
50+
system("limactl shell $example uname");
51+
system("limactl stop $example");
52+
system("limactl delete $example");
53+
}
54+
system("sudo pkill dtrace");
55+
56+
open(my $fh, "<", $opensnoop) or die "Can't read $opensnoop: $!";
57+
while (<$fh>) {
58+
# Only record files opened by limactl or qemu-*
59+
next unless /^\s*\d+\s+\d+\s+(limactl|qemu-)/;
60+
# Ignore files not under /usr/local
61+
next unless s|^.*($install_dir/\S+).*$|$1|s;
62+
# Skip files that don't exist
63+
next unless -f;
64+
record($_);
65+
}
66+
67+
my %deps;
68+
print "$_ $deps{$_}\n" for sort keys %deps;
69+
print "\n";
70+
71+
my $dist = "lima-and-qemu";
72+
system("rm -rf /tmp/$dist");
73+
74+
# Copy all files to /tmp tree and make all dylib references relative to the
75+
# /usr/local/bin directory using @executable_path/..
76+
my %resign;
77+
for my $file (keys %deps) {
78+
my $copy = $file =~ s|^$install_dir|/tmp/$dist|r;
79+
system("mkdir -p " . dirname($copy));
80+
system("cp -R $file $copy");
81+
next if -l $file;
82+
next unless qx(file $copy) =~ /Mach-O/;
83+
84+
open(my $fh, "otool -L $file |") or die "Failed to run 'otool -L $file': $!";
85+
while (<$fh>) {
86+
my($dylib) = m|$install_dir/(\S+)| or next;
87+
my $grep = "";
88+
if ($file =~ m|bin/qemu-system-x86_64$|) {
89+
# qemu-system-* is already signed with an entitlement to use the hypervisor framework
90+
$grep = "| grep -v 'will invalidate the code signature'";
91+
$resign{$copy}++;
92+
}
93+
system "install_name_tool -change $install_dir/$dylib \@executable_path/../$dylib $copy 2>&1 $grep";
94+
}
95+
close($fh);
96+
}
97+
# Replace invalidated signatures
98+
for my $file (keys %resign) {
99+
system("codesign --sign - --force --preserve-metadata=entitlements $file");
100+
}
101+
102+
unlink("$repo_root/$dist.tar.gz");
103+
my $files = join(" ", map s|^$install_dir/||r, keys %deps);
104+
system("tar cvfz $repo_root/$dist.tar.gz -C /tmp/$dist $files");
105+
exit;
106+
107+
# File references may involve multiple symlinks that need to be recorded as well, e.g.
108+
#
109+
# /usr/local/opt/libssh/lib/libssh.4.dylib
110+
#
111+
# turns into 2 symlinks and one file:
112+
#
113+
# /usr/local/opt/libssh → ../Cellar/libssh/0.9.5_1
114+
# /usr/local/Cellar/libssh/0.9.5_1/lib/libssh.4.dylib → libssh.4.8.6.dylib
115+
# /usr/local/Cellar/libssh/0.9.5_1/lib/libssh.4.8.6.dylib [394K]
116+
117+
my %seen;
118+
sub record {
119+
my $dep = shift;
120+
return if $seen{$dep}++;
121+
$dep =~ s|^/|| or die "$dep is not an absolute path";
122+
my $filename = "";
123+
my @segments = split '/', $dep;
124+
while (@segments) {
125+
my $segment = shift @segments;
126+
my $name = "$filename/$segment";
127+
my $link = readlink $name;
128+
if (defined $link) {
129+
# Record the symlink itself with the link target as the comment
130+
$deps{$name} = "$link";
131+
if ($link =~ m|^/|) {
132+
# Can't support absolute links pointing outside /usr/local
133+
die "$name$link" unless $link =~ m|^$install_dir/|;
134+
$link = join("/", $link, @segments);
135+
} else {
136+
$link = join("/", $filename, $link, @segments);
137+
}
138+
# Re-parse from the start because the link may contain ".." segments
139+
return record($link)
140+
}
141+
if ($segment eq "..") {
142+
$filename = dirname($filename);
143+
} else {
144+
$filename = $name;
145+
}
146+
}
147+
# Use human readable size of the file as the comment:
148+
# $ ls -lh /usr/local/Cellar/libssh/0.9.5_1/lib/libssh.4.8.6.dylib
149+
# -rw-r--r-- 1 jan staff 394K 5 Jan 11:04 /usr/local/Cellar/libssh/0.9.5_1/lib/libssh.4.8.6.dylib
150+
$deps{$filename} = sprintf "[%s]", (split / +/, qx(ls -lh $filename))[4];
151+
}
152+
153+
sub dirname {
154+
shift =~ s|/[^/]+$||r;
155+
}

0 commit comments

Comments
 (0)