Skip to content

Commit e4a4971

Browse files
authored
Add build option to reduce final image size (#16729)
* Reduce SONiC image filesystem size Add a build option to reduce the image size. The image reduction process is affecting the builds in 2 ways: - change some packages that are installed in the rootfs - apply a rootfs reduction script The script itself will perform a few steps: - remove file duplication by leveraging hardlinks - under /usr/share/sonic since the symlinks under the device folder are lost during the build. - under /var/lib/docker since the files there will only be mounted ro - remove some extra files (man, docs, licenses, ...) - some image specific space reduction (only for aboot images currently) The script can later be improved but for now it's reducing the rootfs size by ~30%. * restore fully featured vim package
1 parent 1eae349 commit e4a4971

5 files changed

Lines changed: 279 additions & 3 deletions

File tree

build_debian.sh

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ TRUSTED_GPG_DIR=$BUILD_TOOL_PATH/trusted.gpg.d
5959
exit 1
6060
}
6161

62+
if [ "$IMAGE_TYPE" = "aboot" ]; then
63+
TARGET_BOOTLOADER="aboot"
64+
fi
65+
6266
## Check if not a last stage of RFS build
6367
if [[ $RFS_SPLIT_LAST_STAGE != y ]]; then
6468

@@ -68,9 +72,14 @@ if [[ -d $FILESYSTEM_ROOT ]]; then
6872
fi
6973
mkdir -p $FILESYSTEM_ROOT
7074
mkdir -p $FILESYSTEM_ROOT/$PLATFORM_DIR
71-
mkdir -p $FILESYSTEM_ROOT/$PLATFORM_DIR/grub
7275
touch $FILESYSTEM_ROOT/$PLATFORM_DIR/firsttime
7376

77+
bootloader_packages=""
78+
if [ "$TARGET_BOOTLOADER" != "aboot" ]; then
79+
mkdir -p $FILESYSTEM_ROOT/$PLATFORM_DIR/grub
80+
bootloader_packages="grub2-common"
81+
fi
82+
7483
## ensure proc is mounted
7584
sudo mount proc /proc -t proc || true
7685

@@ -365,7 +374,7 @@ sudo LANG=C DEBIAN_FRONTEND=noninteractive chroot $FILESYSTEM_ROOT apt-get -y in
365374
gdisk \
366375
sysfsutils \
367376
squashfs-tools \
368-
grub2-common \
377+
$bootloader_packages \
369378
screen \
370379
hping3 \
371380
tcptraceroute \
@@ -825,6 +834,17 @@ sudo mkdir -p $FILESYSTEM_ROOT/var/lib/docker
825834
sudo rm -f $FILESYSTEM_ROOT/etc/resolvconf/resolv.conf.d/original
826835
sudo cp files/image_config/resolv-config/resolv.conf.head $FILESYSTEM_ROOT/etc/resolvconf/resolv.conf.d/head
827836

837+
## Optimize filesystem size
838+
if [ "$BUILD_REDUCE_IMAGE_SIZE" = "y" ]; then
839+
sudo scripts/build-optimize-fs-size.py "$FILESYSTEM_ROOT" \
840+
--image-type "$IMAGE_TYPE" \
841+
--hardlinks var/lib/docker \
842+
--hardlinks usr/share/sonic/device \
843+
--remove-docs \
844+
--remove-mans \
845+
--remove-licenses
846+
fi
847+
828848
sudo mksquashfs $FILESYSTEM_ROOT $FILESYSTEM_SQUASHFS -comp zstd -b 1M -e boot -e var/lib/docker -e $PLATFORM_DIR
829849

830850
## Reduce /boot permission

files/build_templates/sonic_debian_extension.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ sudo dpkg --root=$FILESYSTEM_ROOT -i $debs_path/libnss-radius_*.deb || \
300300
sudo sed -i -e '/^passwd/s/ radius//' $FILESYSTEM_ROOT/etc/nsswitch.conf
301301

302302
# Install a custom version of kdump-tools (and its dependencies via 'apt-get -y install -f')
303-
if [[ $TARGET_BOOTLOADER == grub ]]; then
303+
if [ "$TARGET_BOOTLOADER" != uboot ]; then
304304
sudo DEBIAN_FRONTEND=noninteractive dpkg --root=$FILESYSTEM_ROOT -i $debs_path/kdump-tools_*.deb || \
305305
sudo LANG=C DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true chroot $FILESYSTEM_ROOT apt-get -q --no-install-suggests --no-install-recommends install
306306
cat $IMAGE_CONFIGS/kdump/kdump-tools | sudo tee -a $FILESYSTEM_ROOT/etc/default/kdump-tools > /dev/null

rules/config

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,3 +315,5 @@ SONIC_OS_VERSION ?= 11
315315
# PIP timeout for http connection
316316
PIP_HTTP_TIMEOUT ?= 60
317317

318+
# BUILD_REDUCE_IMAGE_SIZE - reduce the image size as much as possbible
319+
BUILD_REDUCE_IMAGE_SIZE = n

scripts/build-optimize-fs-size.py

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import hashlib
5+
import os
6+
import shutil
7+
import subprocess
8+
import sys
9+
10+
from collections import defaultdict
11+
from functools import cached_property
12+
13+
DRY_RUN = False
14+
def enable_dry_run(enabled):
15+
global DRY_RUN # pylint: disable=global-statement
16+
DRY_RUN = enabled
17+
18+
class File:
19+
def __init__(self, path):
20+
self.path = path
21+
22+
def __str__(self):
23+
return self.path
24+
25+
def rmtree(self):
26+
if DRY_RUN:
27+
print(f'rmtree {self.path}')
28+
return
29+
shutil.rmtree(self.path)
30+
31+
def hardlink(self, src):
32+
if DRY_RUN:
33+
print(f'hardlink {self.path} {src}')
34+
return
35+
st = self.stats
36+
os.remove(self.path)
37+
os.link(src.path, self.path)
38+
os.chmod(self.path, st.st_mode)
39+
os.chown(self.path, st.st_uid, st.st_gid)
40+
os.utime(self.path, times=(st.st_atime, st.st_mtime))
41+
42+
@property
43+
def name(self):
44+
return os.path.basename(self.path)
45+
46+
@cached_property
47+
def stats(self):
48+
return os.stat(self.path)
49+
50+
@cached_property
51+
def size(self):
52+
return self.stats.st_size
53+
54+
@cached_property
55+
def checksum(self):
56+
with open(self.path, 'rb') as f:
57+
return hashlib.md5(f.read()).hexdigest()
58+
59+
class FileManager:
60+
def __init__(self, path):
61+
self.path = path
62+
self.files = []
63+
self.folders = []
64+
self.nindex = defaultdict(list)
65+
self.cindex = defaultdict(list)
66+
67+
def add_file(self, path):
68+
if not os.path.isfile(path) or os.path.islink(path):
69+
return
70+
f = File(path)
71+
self.files.append(f)
72+
73+
def load_tree(self):
74+
self.files = []
75+
self.folders = []
76+
for root, _, files in os.walk(self.path):
77+
self.folders.append(File(root))
78+
for f in files:
79+
self.add_file(os.path.join(root, f))
80+
print(f'loaded {len(self.files)} files and {len(self.folders)} folders')
81+
82+
def generate_index(self):
83+
print('Computing file hashes')
84+
for f in self.files:
85+
self.nindex[f.name].append(f)
86+
self.cindex[(f.name, f.checksum)].append(f)
87+
88+
def create_hardlinks(self):
89+
print('Creating hard links')
90+
for files in self.cindex.values():
91+
if len(files) <= 1:
92+
continue
93+
orig = files[0]
94+
for f in files[1:]:
95+
f.hardlink(orig)
96+
97+
class FsRoot:
98+
def __init__(self, path):
99+
self.path = path
100+
101+
def iter_fsroots(self):
102+
yield self.path
103+
dimgpath = os.path.join(self.path, 'var/lib/docker/overlay2')
104+
for layer in os.listdir(dimgpath):
105+
yield os.path.join(dimgpath, layer, 'diff')
106+
107+
def collect_fsroot_size(self):
108+
cmd = ['du', '-sb', self.path]
109+
p = subprocess.run(cmd, text=True, check=False,
110+
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
111+
return int(p.stdout.split()[0])
112+
113+
def _remove_root_paths(self, relpaths):
114+
for root in self.iter_fsroots():
115+
for relpath in relpaths:
116+
path = os.path.join(root, relpath)
117+
if os.path.isdir(path):
118+
if DRY_RUN:
119+
print(f'rmtree {path}')
120+
else:
121+
shutil.rmtree(path)
122+
123+
def remove_docs(self):
124+
self._remove_root_paths([
125+
'usr/share/doc',
126+
'usr/share/doc-base',
127+
'usr/local/share/doc',
128+
'usr/local/share/doc-base',
129+
])
130+
131+
def remove_mans(self):
132+
self._remove_root_paths([
133+
'usr/share/man',
134+
'usr/local/share/man',
135+
])
136+
137+
def remove_licenses(self):
138+
self._remove_root_paths([
139+
'usr/share/common-licenses',
140+
])
141+
142+
def hardlink_under(self, path):
143+
fm = FileManager(os.path.join(self.path, path))
144+
fm.load_tree()
145+
fm.generate_index()
146+
fm.create_hardlinks()
147+
148+
def remove_platforms(self, filter_func):
149+
devpath = os.path.join(self.path, 'usr/share/sonic/device')
150+
for platform in os.listdir(devpath):
151+
if not filter_func(platform):
152+
path = os.path.join(devpath, platform)
153+
if DRY_RUN:
154+
print(f'rmtree platform {path}')
155+
else:
156+
shutil.rmtree(path)
157+
158+
def remove_modules(self, modules):
159+
modpath = os.path.join(self.path, 'lib/modules')
160+
kversion = os.listdir(modpath)[0]
161+
kmodpath = os.path.join(modpath, kversion)
162+
for module in modules:
163+
path = os.path.join(kmodpath, module)
164+
if os.path.isdir(path):
165+
if DRY_RUN:
166+
print(f'rmtree module {path}')
167+
else:
168+
shutil.rmtree(path)
169+
170+
def remove_firmwares(self, firmwares):
171+
fwpath = os.path.join(self.path, 'lib/firmware')
172+
for fw in firmwares:
173+
path = os.path.join(fwpath, fw)
174+
if os.path.isdir(path):
175+
if DRY_RUN:
176+
print(f'rmtree firmware {path}')
177+
else:
178+
shutil.rmtree(path)
179+
180+
181+
def specialize_aboot_image(self):
182+
fp = lambda p: '-' not in p or 'arista' in p or 'common' in p
183+
self.remove_platforms(fp)
184+
self.remove_modules([
185+
'kernel/drivers/gpu',
186+
'kernel/drivers/infiniband',
187+
])
188+
self.remove_firmwares([
189+
'amdgpu',
190+
'i915',
191+
'mediatek',
192+
'nvidia',
193+
'radeon',
194+
])
195+
196+
def specialize_image(self, image_type):
197+
if image_type == 'aboot':
198+
self.specialize_aboot_image()
199+
200+
def parse_args(args):
201+
parser = argparse.ArgumentParser()
202+
parser.add_argument('fsroot',
203+
help="path to the fsroot build folder")
204+
parser.add_argument('-s', '--stats', action='store_true',
205+
help="show space statistics")
206+
parser.add_argument('--hardlinks', action='append',
207+
help="path where similar files need to be hardlinked")
208+
parser.add_argument('--remove-docs', action='store_true',
209+
help="remove documentation")
210+
parser.add_argument('--remove-licenses', action='store_true',
211+
help="remove license files")
212+
parser.add_argument('--remove-mans', action='store_true',
213+
help="remove manpages")
214+
parser.add_argument('--image-type', default=None,
215+
help="type of image being built")
216+
parser.add_argument('--dry-run', action='store_true',
217+
help="only display what would happen")
218+
return parser.parse_args(args)
219+
220+
def main(args):
221+
args = parse_args(args)
222+
223+
enable_dry_run(args.dry_run)
224+
225+
fs = FsRoot(args.fsroot)
226+
if args.stats:
227+
begin = fs.collect_fsroot_size()
228+
print(f'fsroot size is {begin} bytes')
229+
230+
if args.remove_docs:
231+
fs.remove_docs()
232+
233+
if args.remove_mans:
234+
fs.remove_mans()
235+
236+
if args.remove_licenses:
237+
fs.remove_licenses()
238+
239+
if args.image_type:
240+
fs.specialize_image(args.image_type)
241+
242+
for path in args.hardlinks:
243+
fs.hardlink_under(path)
244+
245+
if args.stats:
246+
end = fs.collect_fsroot_size()
247+
pct = 100 - end / begin * 100
248+
print(f'fsroot reduced to {end} from {begin} {pct:.2f}')
249+
250+
return 0
251+
252+
if __name__ == '__main__':
253+
sys.exit(main(sys.argv[1:]))

slave.mk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1536,6 +1536,7 @@ $(addprefix $(TARGET_PATH)/, $(SONIC_INSTALLERS)) : $(TARGET_PATH)/% : \
15361536
SONIC_VERSION_CACHE=$(SONIC_VERSION_CACHE) \
15371537
MULTIARCH_QEMU_ENVIRON=$(MULTIARCH_QEMU_ENVIRON) \
15381538
CROSS_BUILD_ENVIRON=$(CROSS_BUILD_ENVIRON) \
1539+
BUILD_REDUCE_IMAGE_SIZE=$(BUILD_REDUCE_IMAGE_SIZE) \
15391540
MASTER_KUBERNETES_VERSION=$(MASTER_KUBERNETES_VERSION) \
15401541
MASTER_KUBERNETES_CONTAINER_IMAGE_VERSION=$(MASTER_KUBERNETES_CONTAINER_IMAGE_VERSION) \
15411542
MASTER_PAUSE_VERSION=$(MASTER_PAUSE_VERSION) \

0 commit comments

Comments
 (0)