f4e45e91b7f86c2c392d68b16d1060a1b7f1e663
[grml2usb.git] / grml2usb.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 """
4 grml2usb
5 ~~~~~~~~
6
7 This script installs a grml system (running system / ISO[s]) to a USB device
8
9 :copyright: (c) 2009 by Michael Prokop <mika@grml.org>
10 :license: GPL v2 or any later version
11 :bugreports: http://grml.org/bugs/
12
13 TODO
14 ----
15
16 * verify that the specified device is really an USB device (/sys/devices/*/removable_)
17 * validate partition schema/layout:
18   -> bootable flag?
19   -> fat16 partition if using syslinux
20 * implement missing options (--kernel, --initrd, --uninstall,...)
21 * improve error handling :)
22 * implement logic for storing information about copied files
23   -> register every single file?
24 * get rid of all TODOs in code :)
25 * graphical version? :)
26 """
27
28 from __future__ import with_statement
29 import os, re, subprocess, sys, tempfile
30 from optparse import OptionParser
31 from os.path import exists, join, abspath
32 from os import pathsep
33 from inspect import isroutine, isclass
34 import logging
35
36 PROG_VERSION = "0.0.1"
37
38 skip_mbr = False # By default we don't want to skip it; TODO - can we get rid of that?
39
40 # cmdline parsing
41 usage = "Usage: %prog [options] <[ISO[s] | /live/image]> </dev/ice>\n\
42 \n\
43 %prog installs a grml ISO to an USB device to be able to boot from it.\n\
44 Make sure you have at least a grml ISO or a running grml system (/live/image),\n\
45 syslinux (just run 'aptitude install syslinux' on Debian-based systems)\n\
46 and root access."
47
48 parser = OptionParser(usage=usage)
49 parser.add_option("--bootoptions", dest="bootoptions",
50                   action="store", type="string",
51                   help="use specified bootoptions as defaut")
52 parser.add_option("--bootloader-only", dest="bootloaderonly", action="store_true",
53                   help="do not copy files only but just install a bootloader")
54 parser.add_option("--copy-only", dest="copyonly", action="store_true",
55                   help="copy files only and do not install bootloader")
56 parser.add_option("--dry-run", dest="dryrun", action="store_true",
57                   help="do not actually execute any commands")
58 parser.add_option("--fat16", dest="fat16", action="store_true",
59                   help="format specified partition with FAT16")
60 parser.add_option("--force", dest="force", action="store_true",
61                   help="force any actions requiring manual interaction")
62 parser.add_option("--grub", dest="grub", action="store_true",
63                   help="install grub bootloader instead of syslinux")
64 parser.add_option("--initrd", dest="initrd", action="store", type="string",
65                   help="install specified initrd instead of the default")
66 parser.add_option("--kernel", dest="kernel", action="store", type="string",
67                   help="install specified kernel instead of the default")
68 parser.add_option("--mbr", dest="mbr", action="store_true",
69                   help="install master boot record (MBR) on the device")
70 parser.add_option("--mountpath", dest="mountpath", action="store_true",
71                   help="install system to specified mount path")
72 parser.add_option("--quiet", dest="quiet", action="store_true",
73                   help="do not output anything than errors on console")
74 parser.add_option("--squashfs", dest="squashfs", action="store", type="string",
75                   help="install specified squashfs file instead of the default")
76 parser.add_option("--uninstall", dest="uninstall", action="store_true",
77                   help="remove grml ISO files")
78 parser.add_option("--verbose", dest="verbose", action="store_true",
79                   help="enable verbose mode")
80 parser.add_option("-v", "--version", dest="version", action="store_true",
81                   help="display version and exit")
82 (options, args) = parser.parse_args()
83
84
85 def get_function_name(obj):
86     if not (isroutine(obj) or isclass(obj)):
87         obj = type(obj)
88     return obj.__module__ + '.' + obj.__name__
89
90
91 def execute(f, *args):
92     """Wrapper for executing a command. Either really executes
93     the command (default) or when using --dry-run commandline option
94     just displays what would be executed."""
95     # demo: execute(subprocess.Popen, (["ls", "-la"]))
96     if options.dryrun:
97         logging.debug('dry-run only: %s(%s)' % (get_function_name(f), ', '.join(map(repr, args))))
98     else:
99         return f(*args)
100
101
102 def is_exe(fpath):
103     """Check whether a given file can be executed
104
105     @fpath: full path to file
106     @return:"""
107     return os.path.exists(fpath) and os.access(fpath, os.X_OK)
108
109
110 def which(program):
111     """Check whether a given program is available in PATH
112
113     @program: name of executable"""
114     fpath, fname = os.path.split(program)
115     if fpath:
116         if is_exe(program):
117             return program
118     else:
119         for path in os.environ["PATH"].split(os.pathsep):
120             exe_file = os.path.join(path, program)
121             if is_exe(exe_file):
122                 return exe_file
123
124     return None
125
126
127 def search_file(filename, search_path='/bin' + pathsep + '/usr/bin'):
128     """Given a search path, find file"""
129     file_found = 0
130     paths = search_path.split(pathsep)
131     for path in paths:
132         for current_dir, directories, files in os.walk(path):
133             if exists(join(current_dir, filename)):
134                 file_found = 1
135                 break
136     if file_found:
137         return abspath(join(current_dir, filename))
138     else:
139         return None
140
141
142 def check_uid_root():
143     """Check for root permissions"""
144     if not os.geteuid()==0:
145         sys.exit("Error: please run this script with uid 0 (root).")
146
147
148 def install_syslinux(device, dry_run=False):
149     # TODO
150     """Install syslinux on specified device."""
151
152     # syslinux -d boot/isolinux /dev/sdb1
153     logging.info("Installing syslinux")
154     logging.debug("syslinux -d boot/syslinux %s" % device)
155     proc = subprocess.Popen(["syslinux", "-d", "boot/syslinux", device])
156     proc.wait()
157     if proc.returncode != 0:
158         raise Exception, "error executing syslinux"
159
160
161 def generate_grub_config(grml_flavour):
162     """Generate grub configuration for use via menu,lst"""
163
164     # TODO
165     # * install main part of configuration just *once* and append
166     #   flavour specific configuration only
167     # * what about systems using grub2 without having grub1 available?
168     # * support grub2?
169
170     return("""\
171 # misc options:
172 timeout 30
173 # color red/blue green/black
174 splashimage=/boot/grub/splash.xpm.gz
175 foreground  = 000000
176 background  = FFCC33
177
178 # define entries:
179 title %(grml_flavour)s  - Default boot (using 1024x768 framebuffer)
180 kernel /boot/release/%(grml_flavour)s/linux26 apm=power-off lang=us vga=791 quiet boot=live nomce module=%(grml_flavour)s
181 initrd /boot/release/%(grml_flavour)s/initrd.gz
182
183 # TODO: extend configuration :)
184 """ % locals())
185
186
187 def generate_isolinux_splash(grml_flavour):
188     """Generate bootsplash for isolinux/syslinux"""
189
190     # TODO
191     # * adjust last bootsplash line
192
193     grml_name = grml_flavour
194
195     return("""\
196 \ f17\f\18/boot/syslinux/logo.16
197
198 Some information and boot options available via keys F2 - F10. http://grml.org/
199 %(grml_name)s
200 """ % locals())
201
202 def generate_main_syslinux_config(grml_flavour, grml_bootoptions):
203     """Generate main configuration for use in syslinux.cfg"""
204
205     # TODO
206     # * install main part of configuration just *once* and append
207     #   flavour specific configuration only
208     # * unify isolinux and syslinux setup ("INCLUDE /boot/...")
209     #   as far as possible
210
211     return("""\
212 ## main syslinux configuration - generated by grml2usb
213 # use this to control the bootup via a serial port
214 # SERIAL 0 9600
215 DEFAULT grml
216 TIMEOUT 300
217 PROMPT 1
218 DISPLAY /boot/syslinux/boot.msg
219 F1 /boot/syslinux/boot.msg
220 F2 /boot/syslinux/f2
221 F3 /boot/syslinux/f3
222 F4 /boot/syslinux/f4
223 F5 /boot/syslinux/f5
224 F6 /boot/syslinux/f6
225 F7 /boot/syslinux/f7
226 F8 /boot/syslinux/f8
227 F9 /boot/syslinux/f9
228 F10 /boot/syslinux/f10
229 ## end of main configuration
230
231 # flavour specific configuration for grml
232 LABEL  grml
233 KERNEL /boot/release/%(grml_flavour)s/linux26
234 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce quiet module=%(grml_flavour)s %(grml_bootoptions)s
235
236 """ % locals())
237
238 def generate_flavour_specific_syslinux_config(grml_flavour, bootoptions):
239     """Generate flavour specific configuration for use in syslinux.cfg"""
240
241     return("""\
242
243 # flavour specific configuration for %(grml_flavour)s
244 LABEL  %(grml_flavour)s
245 KERNEL /boot/release/%(grml_flavour)s/linux26
246 APPEND initrd=/boot/release/%(grml_flavour)s/initrd.gz apm=power-off boot=live nomce quiet module=%(grml_flavour)s %(bootoptions)s
247 """ % locals())
248
249
250 def install_grub(device, dry_run=False):
251     """Install grub on specified device."""
252     logging.critical("TODO: grub-install %s"  % device)
253
254
255 def install_bootloader(partition, dry_run=False):
256     """Install bootloader on device."""
257
258     # Install bootloader on the device (/dev/sda),
259     # not on the partition itself (/dev/sda1)?
260 #    if partition[-1:].isdigit():
261 #        device = re.match(r'(.*?)\d*$', partition).group(1)
262 #    else:
263 #        device = partition
264
265     if options.grub:
266         install_grub(partition, dry_run)
267     else:
268         install_syslinux(partition, dry_run)
269
270
271 def is_writeable(device):
272     """Check if the device is writeable for the current user"""
273
274     if not device:
275         return False
276         #raise Exception, "no device for checking write permissions"
277
278     if not os.path.exists(device):
279         return False
280
281     return os.access(device, os.W_OK) and os.access(device, os.R_OK)
282
283 def install_mbr(device, dry_run=False):
284     """Install a default master boot record on given device
285
286     @device: device where MBR should be installed to"""
287
288     if not is_writeable(device):
289         raise IOError, "device not writeable for user"
290
291     lilo = '/grml/git/grml2usb/lilo/lilo.static' # FIXME
292
293     if not is_exe(lilo):
294         raise Exception, "lilo executable not available."
295
296     # to support -A for extended partitions:
297     logging.info("Installing MBR")
298     logging.debug("%s -S /dev/null -M %s ext" % (lilo, device))
299     proc = subprocess.Popen([lilo, "-S", "/dev/null", "-M", device, "ext"])
300     proc.wait()
301     if proc.returncode != 0:
302         raise Exception, "error executing lilo"
303
304     # activate partition:
305     logging.debug("%s -S /dev/null -A %s 1" % (lilo, device))
306     if not dry_run:
307         proc = subprocess.Popen([lilo, "-S", "/dev/null", "-A", device, "1"])
308         proc.wait()
309         if proc.returncode != 0:
310             raise Exception, "error executing lilo"
311
312     # lilo's mbr is broken, use the one from syslinux instead:
313     logging.debug("cat /usr/lib/syslinux/mbr.bin > %s" % device)
314     if not dry_run:
315         try:
316             # TODO use Popen instead?
317             retcode = subprocess.call("cat /usr/lib/syslinux/mbr.bin > "+ device, shell=True)
318             if retcode < 0:
319                 logging.critical("Error copying MBR to device (%s)" % retcode)
320         except OSError, error:
321             logging.critical("Execution failed:", error)
322
323
324 def mount(source, target, options):
325     """Mount specified source on given target
326
327     @source: name of device/ISO that should be mounted
328     @target: directory where the ISO should be mounted to
329     @options: mount specific options"""
330
331 #   notice: dry_run does not work here, as we have to locate files, identify flavour,...
332     logging.debug("mount %s %s %s" % (options, source, target))
333     proc = subprocess.Popen(["mount"] + list(options) + [source, target])
334     proc.wait()
335     if proc.returncode != 0:
336         raise Exception, "Error executing mount"
337
338 def unmount(directory):
339     """Unmount specified directory
340
341     @directory: directory where something is mounted on and which should be unmounted"""
342
343     logging.debug("umount %s" % directory)
344     proc = subprocess.Popen(["umount"] + [directory])
345     proc.wait()
346     if proc.returncode != 0:
347         raise Exception, "Error executing umount"
348
349
350 def check_for_vat(partition):
351     """Check whether specified partition is a valid VFAT/FAT16 filesystem
352
353     @partition: device name of partition"""
354
355     try:
356         udev_info = subprocess.Popen(["/lib/udev/vol_id", "-t",
357             partition],stdout=subprocess.PIPE, stderr=subprocess.PIPE)
358         filesystem = udev_info.communicate()[0].rstrip()
359
360         if udev_info.returncode == 2:
361             logging.critical("failed to read device %s - wrong UID / permissions?" % partition)
362             return 1
363
364         if filesystem != "vfat":
365             return 1
366
367         # TODO
368         # * check for ID_FS_VERSION=FAT16 as well?
369
370     except OSError:
371         logging.critical("Sorry, /lib/udev/vol_id not available.")
372         return 1
373
374
375 def mkdir(directory):
376     """Simple wrapper around os.makedirs to get shell mkdir -p behaviour"""
377
378     if not os.path.isdir(directory):
379         try:
380             os.makedirs(directory)
381         except OSError:
382             # just silently pass as it's just fine it the directory exists
383             pass
384
385
386 def copy_grml_files(grml_flavour, iso_mount, target, dry_run=False):
387     """Copy files from ISO on given target"""
388
389     # TODO
390     # * provide alternative search_file() if file information is stored in a config.ini file?
391     # * catch "install: .. No space left on device" & CO
392     # * abstract copy logic to make the code shorter and get rid of spaghetti ;)
393
394     logging.info("Copying files. This might take a while....")
395
396     squashfs = search_file(grml_flavour + '.squashfs', iso_mount)
397     squashfs_target = target + '/live/'
398     execute(mkdir, squashfs_target)
399
400     # use install(1) for now to make sure we can write the files afterwards as normal user as well
401     logging.debug("cp %s %s" % (squashfs, target + '/live/' + grml_flavour + '.squashfs'))
402     proc = execute(subprocess.Popen, ["install", "--mode=664", squashfs, squashfs_target + grml_flavour + ".squashfs"])
403     proc.wait()
404
405     filesystem_module = search_file('filesystem.module', iso_mount)
406     logging.debug("cp %s %s" % (filesystem_module, squashfs_target + grml_flavour + '.module'))
407     proc = execute(subprocess.Popen, ["install", "--mode=664", filesystem_module, squashfs_target + grml_flavour + '.module'])
408     proc.wait()
409
410     release_target = target + '/boot/release/' + grml_flavour
411     execute(mkdir, release_target)
412
413     kernel = search_file('linux26', iso_mount)
414     logging.debug("cp %s %s" % (kernel, release_target + '/linux26'))
415     proc = execute(subprocess.Popen, ["install", "--mode=664", kernel, release_target + '/linux26'])
416     proc.wait()
417
418     initrd = search_file('initrd.gz', iso_mount)
419     logging.debug("cp %s %s" % (initrd, release_target + '/initrd.gz'))
420     proc = execute(subprocess.Popen, ["install", "--mode=664", initrd, release_target + '/initrd.gz'])
421     proc.wait()
422
423     if not options.copyonly:
424         syslinux_target = target + '/boot/syslinux/'
425         execute(mkdir, syslinux_target)
426
427         logo = search_file('logo.16', iso_mount)
428         logging.debug("cp %s %s" % (logo, syslinux_target + 'logo.16'))
429         proc = execute(subprocess.Popen, ["install", "--mode=664", logo, syslinux_target + 'logo.16'])
430         proc.wait()
431
432         for ffile in 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10':
433             bootsplash = search_file(ffile, iso_mount)
434             logging.debug("cp %s %s" % (bootsplash, syslinux_target + ffile))
435             proc = execute(subprocess.Popen, ["install", "--mode=664", bootsplash, syslinux_target + ffile])
436             proc.wait()
437
438         grub_target = target + '/boot/grub/'
439         execute(mkdir, grub_target)
440
441         logging.debug("cp /grml/git/grml2usb/grub/splash.xpm.gz %s" % grub_target + 'splash.xpm.gz') # FIXME - path of grub
442         proc = execute(subprocess.Popen, ["install", "--mode=664", '/grml/git/grml2usb/grub/splash.xpm.gz', grub_target + 'splash.xpm.gz']) # FIXME
443         proc.wait()
444
445         logging.debug("cp /grml/git/grml2usb/grub/stage2_eltorito to %s" % grub_target + 'stage2_eltorito') # FIXME - path of grub
446         proc = execute(subprocess.Popen, ["install", "--mode=664", '/grml/git/grml2usb/grub/stage2_eltorito', grub_target + 'stage2_eltorito']) # FIXME
447         proc.wait()
448
449         if not dry_run:
450             logging.debug("Generating grub configuration") # % grub_target + 'menu.lst')
451             #with open("...", "w") as f:
452             #f.write("bla bla bal")
453             grub_config_file = open(grub_target + 'menu.lst', 'w')
454             grub_config_file.write(generate_grub_config(grml_flavour))
455             grub_config_file.close()
456
457             logging.info("Generating syslinux configuration") # % syslinux_target + 'syslinux.cfg')
458             syslinux_cfg = syslinux_target + 'syslinux.cfg'
459
460             # install main configuration only *once*, no matter how many ISOs we have:
461             if os.path.isfile(syslinux_cfg):
462                 string = open(syslinux_cfg).readline()
463                 if not re.match("## main syslinux configuration", string):
464                     syslinux_config_file = open(syslinux_cfg, 'w')
465                     syslinux_config_file.write(generate_main_syslinux_config(grml_flavour, "")) # FIXME - bootoptions
466                     syslinux_config_file.close()
467             else:
468                     syslinux_config_file = open(syslinux_cfg, 'w')
469                     syslinux_config_file.write(generate_main_syslinux_config(grml_flavour, "")) # FIXME - bootoptions
470                     syslinux_config_file.close()
471
472             # install flavour specific configuration only *once* as well
473             # ugly - I'm pretty sure this could be smoother...
474             flavour_config = True
475             if os.path.isfile(syslinux_cfg):
476                 string = open(syslinux_cfg).readlines()
477                 flavour = re.compile("^# flavour specific configuration for %s"  % re.escape(grml_flavour))
478                 for line in string:
479                     if flavour.match(line):
480                         flavour_config = False
481
482             if flavour_config:
483                 syslinux_config_file = open(syslinux_cfg, 'a')
484                 syslinux_config_file.write(generate_flavour_specific_syslinux_config(grml_flavour, "")) # FIXME - bootoptions
485                 syslinux_config_file.close( )
486
487             logging.debug("Generating isolinux/syslinux splash %s" % syslinux_target + 'boot.msg')
488             isolinux_splash = open(syslinux_target + 'boot.msg', 'w')
489             isolinux_splash.write(generate_isolinux_splash(grml_flavour))
490             isolinux_splash.close( )
491
492
493     # make sure we are sync before continuing
494     proc = subprocess.Popen(["sync"])
495     proc.wait()
496
497 def uninstall_files(device):
498     """Get rid of all grml files on specified device"""
499
500     # TODO
501     logging.critical("TODO: %s" % device)
502
503
504 def identify_grml_flavour(mountpath):
505     """Get name of grml flavour
506
507     @mountpath: path where the grml ISO is mounted to
508     @return: name of grml-flavour"""
509
510     version_file = search_file('grml-version', mountpath)
511
512     if version_file == "":
513         logging.critical("Error: could not find grml-version file.")
514         raise
515
516     try:
517         tmpfile = open(version_file, 'r')
518         grml_info = tmpfile.readline()
519         grml_flavour = re.match(r'[\w-]*', grml_info).group()
520     except TypeError:
521         raise
522     except:
523         logging.critical("Unexpected error:", sys.exc_info()[0])
524         raise
525
526     return grml_flavour
527
528 def handle_iso(iso, device):
529     """TODO
530     """
531
532     logging.info("Using ISO %s" % iso)
533
534     if os.path.isdir(iso):
535         logging.critical("TODO: /live/image handling not yet implemented") # TODO
536     else:
537         iso_mountpoint = tempfile.mkdtemp()
538         remove_iso_mountpoint = True
539         mount(iso, iso_mountpoint, ["-o", "loop", "-t", "iso9660"])
540
541         if os.path.isdir(device):
542             logging.info("Specified target is a directory, not mounting therefore.")
543             device_mountpoint = device
544             remove_device_mountpoint = False
545             skip_mbr = True
546
547         else:
548             device_mountpoint = tempfile.mkdtemp()
549             remove_device_mountpoint = True
550             mount(device, device_mountpoint, "")
551
552         try:
553             grml_flavour = identify_grml_flavour(iso_mountpoint)
554             logging.info("Identified grml flavour \"%s\"." % grml_flavour)
555             copy_grml_files(grml_flavour, iso_mountpoint, device_mountpoint, dry_run=options.dryrun)
556         except TypeError:
557             logging.critical("Fatal: something happend - TODO")
558             sys.exit(1)
559         finally:
560             if os.path.isdir(iso_mountpoint) and remove_iso_mountpoint:
561                 unmount(iso_mountpoint)
562                 os.rmdir(iso_mountpoint)
563
564             if remove_device_mountpoint:
565                 unmount(device_mountpoint)
566
567                 if os.path.isdir(device_mountpoint):
568                     os.rmdir(device_mountpoint)
569
570         # grml_flavour_short = grml_flavour.replace('-','')
571         # logging.debug("grml_flavour_short = %s" % grml_flavour_short)
572
573
574 def main():
575     """Main function [make pylint happy :)]"""
576
577     if options.version:
578         print os.path.basename(sys.argv[0]) + " " + PROG_VERSION
579         sys.exit(0)
580
581     if len(args) < 2:
582         parser.error("invalid usage")
583
584     if options.verbose:
585         FORMAT = "%(asctime)-15s %(message)s"
586         logging.basicConfig(level=logging.DEBUG, format=FORMAT)
587     elif options.quiet:
588         FORMAT = "Critial: %(message)s"
589         logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
590     else:
591         FORMAT = "Info: %(message)s"
592         logging.basicConfig(level=logging.INFO, format=FORMAT)
593
594     if options.dryrun:
595         logging.info("Running in simulate mode as requested via option dry-run.")
596     else:
597         check_uid_root()
598
599     device = args[len(args) - 1]
600     isos = args[0:len(args) - 1]
601
602     # make sure we can replace old grml2usb script and warn user when using old way of life:
603     if device.startswith("/mnt/external") or device.startswith("/mnt/usb"):
604         print "Warning: the semantics of grml2usb has changed."
605         print "Instead of using grml2usb /path/to/iso %s you might" % device
606         print "want to use grml2usb /path/to/iso /dev/... instead."
607         print "Please check out the grml2usb manpage for details."
608         f = raw_input("Do you really want to continue? y/N ")
609         if f == "y" or f == "Y":
610            pass
611         else:
612             sys.exit(1)
613
614     if not which("syslinux"):
615         logging.critical('Sorry, syslinux not available. Exiting.')
616         logging.critical('Please install syslinux or consider using the --grub option.')
617         sys.exit(1)
618
619     # TODO
620     # * check for valid blockdevice, vfat and mount functions
621     # if device is not None:
622         # check_for_vat(device)
623
624     for iso in isos:
625         handle_iso(iso, device)
626
627     if options.mbr and not skip_mbr:
628
629         # make sure we install MBR on /dev/sdX and not /dev/sdX#
630         if device[-1:].isdigit():
631             mbr_device = re.match(r'(.*?)\d*$', device).group(1)
632
633         try:
634             install_mbr(mbr_device, dry_run=options.dryrun)
635         except IOError, error:
636             logging.critical("Execution failed:", error)
637             sys.exit(1)
638         except Exception, error:
639             logging.critical("Execution failed:", error)
640             sys.exit(1)
641
642     if options.copyonly:
643         logging.info("Not installing bootloader and its files as requested via option copyonly.")
644     else:
645         install_bootloader(device, dry_run=options.dryrun)
646
647     logging.info("Finished execution of grml2usb (%s). Have fun with your grml system." % PROG_VERSION)
648
649 if __name__ == "__main__":
650     try:
651         main()
652     except KeyboardInterrupt:
653         print "TODO / FIXME: handle me! :)"
654
655 ## END OF FILE #################################################################
656 # vim:foldmethod=marker expandtab ai ft=python tw=120 fileencoding=utf-8