Add F2FS support for apexer

The new flag --payload_fs_type enables the user to choose between ext4
and f2fs for apex_payload.img

Test: ./runtests.sh

Bug: 158453869
Change-Id: I2858d834924c5cec0f809c4f5c8b2e622b8fdf47
(cherry picked from commit 856d59d5dace3179b836e3b4bfb1f4c9bf4d653e)
diff --git a/apexer/Android.bp b/apexer/Android.bp
index 603ea46..b41a632 100644
--- a/apexer/Android.bp
+++ b/apexer/Android.bp
@@ -21,6 +21,8 @@
       "resize2fs",
       "sefcontext_compile",
       "zipalign",
+      "make_f2fs",
+      "sload_f2fs",
       // TODO(b/124476339) apex doesn't follow 'required' dependencies so we need to include this
       // manually for 'avbtool'.
       "fec",
diff --git a/apexer/apexer.py b/apexer/apexer.py
index 8bd01bf..3f34fe9 100644
--- a/apexer/apexer.py
+++ b/apexer/apexer.py
@@ -102,6 +102,13 @@
       choices=['zip', 'image'],
       help='type of APEX payload being built "zip" or "image"')
   parser.add_argument(
+      '--payload_fs_type',
+      metavar='FS_TYPE',
+      required=False,
+      default='ext4',
+      choices=['ext4', 'f2fs'],
+      help='type of filesystem being used for payload image "ext4" or "f2fs"')
+  parser.add_argument(
       '--override_apk_package_name',
       required=False,
       help='package name of the APK container. Default is the apex name in --manifest.'
@@ -163,7 +170,7 @@
   parser.add_argument(
       '--unsigned_payload_only',
       action='store_true',
-      help="""Outputs the unsigned payload image/zip only. Also, setting this flag implies 
+      help="""Outputs the unsigned payload image/zip only. Also, setting this flag implies
                                     --payload_only is set too."""
   )
   parser.add_argument(
@@ -183,7 +190,7 @@
                   ':'.join(tool_path_list))
 
 
-def RunCommand(cmd, verbose=False, env=None):
+def RunCommand(cmd, verbose=False, env=None, expected_return_values={0}):
   env = env or {}
   env.update(os.environ.copy())
 
@@ -195,10 +202,10 @@
       cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env)
   output, _ = p.communicate()
 
-  if verbose or p.returncode is not 0:
+  if verbose or p.returncode not in expected_return_values:
     print(output.rstrip())
 
-  assert p.returncode is 0, 'Failed to execute: ' + ' '.join(cmd)
+  assert p.returncode in expected_return_values, 'Failed to execute: ' + ' '.join(cmd)
 
   return (output, p.returncode)
 
@@ -379,6 +386,9 @@
   if args.logging_parent:
     build_info.logging_parent = args.logging_parent
 
+  if args.payload_type == 'image':
+    build_info.payload_fs_type = args.payload_fs_type
+
   return build_info
 
 def AddLoggingParent(android_manifest, logging_parent_value):
@@ -451,9 +461,8 @@
     print("Cannot read manifest file: '" + args.manifest + "'")
     return False
 
-  # create an empty ext4 image that is sufficiently big
-  # sufficiently big = size + 16MB margin
-  size_in_mb = (GetDirSize(args.input_dir) / (1024 * 1024)) + 16
+  # create an empty image that is sufficiently big
+  size_in_mb = (GetDirSize(args.input_dir) / (1024 * 1024))
 
   content_dir = os.path.join(work_dir, 'content')
   os.mkdir(content_dir)
@@ -480,66 +489,110 @@
       return False
     img_file = os.path.join(content_dir, 'apex_payload.img')
 
-    # margin is for files that are not under args.input_dir. this consists of
-    # n inodes for apex_manifest files and 11 reserved inodes for ext4.
-    # TOBO(b/122991714) eliminate these details. use build_image.py which
-    # determines the optimal inode count by first building an image and then
-    # count the inodes actually used.
-    inode_num_margin = GetFilesAndDirsCount(manifests_dir) + 11
-    inode_num = GetFilesAndDirsCount(args.input_dir) + inode_num_margin
+    if args.payload_fs_type == 'ext4':
+      # sufficiently big = size + 16MB margin
+      size_in_mb += 16
 
-    cmd = ['mke2fs']
-    cmd.extend(['-O', '^has_journal'])  # because image is read-only
-    cmd.extend(['-b', str(BLOCK_SIZE)])
-    cmd.extend(['-m', '0'])  # reserved block percentage
-    cmd.extend(['-t', 'ext4'])
-    cmd.extend(['-I', '256'])  # inode size
-    cmd.extend(['-N', str(inode_num)])
-    uu = str(uuid.uuid5(uuid.NAMESPACE_URL, 'www.android.com'))
-    cmd.extend(['-U', uu])
-    cmd.extend(['-E', 'hash_seed=' + uu])
-    cmd.append(img_file)
-    cmd.append(str(size_in_mb) + 'M')
-    with tempfile.NamedTemporaryFile(dir=work_dir, suffix="mke2fs.conf") as conf_file:
-      conf_data = pkgutil.get_data('apexer', 'mke2fs.conf')
-      conf_file.write(conf_data)
-      conf_file.flush()
-      RunCommand(cmd, args.verbose,
-          {"MKE2FS_CONFIG": conf_file.name, 'E2FSPROGS_FAKE_TIME': '1'})
+      # margin is for files that are not under args.input_dir. this consists of
+      # n inodes for apex_manifest files and 11 reserved inodes for ext4.
+      # TOBO(b/122991714) eliminate these details. use build_image.py which
+      # determines the optimal inode count by first building an image and then
+      # count the inodes actually used.
+      inode_num_margin = GetFilesAndDirsCount(manifests_dir) + 11
+      inode_num = GetFilesAndDirsCount(args.input_dir) + inode_num_margin
 
-    # Compile the file context into the binary form
-    compiled_file_contexts = os.path.join(work_dir, 'file_contexts.bin')
-    cmd = ['sefcontext_compile']
-    cmd.extend(['-o', compiled_file_contexts])
-    cmd.append(args.file_contexts)
-    RunCommand(cmd, args.verbose)
+      cmd = ['mke2fs']
+      cmd.extend(['-O', '^has_journal'])  # because image is read-only
+      cmd.extend(['-b', str(BLOCK_SIZE)])
+      cmd.extend(['-m', '0'])  # reserved block percentage
+      cmd.extend(['-t', 'ext4'])
+      cmd.extend(['-I', '256'])  # inode size
+      cmd.extend(['-N', str(inode_num)])
+      uu = str(uuid.uuid5(uuid.NAMESPACE_URL, 'www.android.com'))
+      cmd.extend(['-U', uu])
+      cmd.extend(['-E', 'hash_seed=' + uu])
+      cmd.append(img_file)
+      cmd.append(str(size_in_mb) + 'M')
+      with tempfile.NamedTemporaryFile(dir=work_dir, suffix="mke2fs.conf") as conf_file:
+        conf_data = pkgutil.get_data('apexer', 'mke2fs.conf')
+        conf_file.write(conf_data)
+        conf_file.flush()
+        RunCommand(cmd, args.verbose,
+            {"MKE2FS_CONFIG": conf_file.name, 'E2FSPROGS_FAKE_TIME': '1'})
 
-    # Add files to the image file
-    cmd = ['e2fsdroid']
-    cmd.append('-e')  # input is not android_sparse_file
-    cmd.extend(['-f', args.input_dir])
-    cmd.extend(['-T', '0'])  # time is set to epoch
-    cmd.extend(['-S', compiled_file_contexts])
-    cmd.extend(['-C', args.canned_fs_config])
-    cmd.append('-s')  # share dup blocks
-    cmd.append(img_file)
-    RunCommand(cmd, args.verbose, {'E2FSPROGS_FAKE_TIME': '1'})
+      # Compile the file context into the binary form
+      compiled_file_contexts = os.path.join(work_dir, 'file_contexts.bin')
+      cmd = ['sefcontext_compile']
+      cmd.extend(['-o', compiled_file_contexts])
+      cmd.append(args.file_contexts)
+      RunCommand(cmd, args.verbose)
 
-    cmd = ['e2fsdroid']
-    cmd.append('-e')  # input is not android_sparse_file
-    cmd.extend(['-f', manifests_dir])
-    cmd.extend(['-T', '0'])  # time is set to epoch
-    cmd.extend(['-S', compiled_file_contexts])
-    cmd.extend(['-C', args.canned_fs_config])
-    cmd.append('-s')  # share dup blocks
-    cmd.append(img_file)
-    RunCommand(cmd, args.verbose, {'E2FSPROGS_FAKE_TIME': '1'})
+      # Add files to the image file
+      cmd = ['e2fsdroid']
+      cmd.append('-e')  # input is not android_sparse_file
+      cmd.extend(['-f', args.input_dir])
+      cmd.extend(['-T', '0'])  # time is set to epoch
+      cmd.extend(['-S', compiled_file_contexts])
+      cmd.extend(['-C', args.canned_fs_config])
+      cmd.append('-s')  # share dup blocks
+      cmd.append(img_file)
+      RunCommand(cmd, args.verbose, {'E2FSPROGS_FAKE_TIME': '1'})
 
-    # Resize the image file to save space
-    cmd = ['resize2fs']
-    cmd.append('-M')  # shrink as small as possible
-    cmd.append(img_file)
-    RunCommand(cmd, args.verbose, {'E2FSPROGS_FAKE_TIME': '1'})
+      cmd = ['e2fsdroid']
+      cmd.append('-e')  # input is not android_sparse_file
+      cmd.extend(['-f', manifests_dir])
+      cmd.extend(['-T', '0'])  # time is set to epoch
+      cmd.extend(['-S', compiled_file_contexts])
+      cmd.extend(['-C', args.canned_fs_config])
+      cmd.append('-s')  # share dup blocks
+      cmd.append(img_file)
+      RunCommand(cmd, args.verbose, {'E2FSPROGS_FAKE_TIME': '1'})
+
+      # Resize the image file to save space
+      cmd = ['resize2fs']
+      cmd.append('-M')  # shrink as small as possible
+      cmd.append(img_file)
+      RunCommand(cmd, args.verbose, {'E2FSPROGS_FAKE_TIME': '1'})
+
+    elif args.payload_fs_type == 'f2fs':
+      # F2FS requires a ~100M minimum size (necessary for ART, could be reduced a bit for other)
+      # TODO(b/158453869): relax these requirements for readonly devices
+      size_in_mb += 100
+
+      # Create an empty image
+      cmd = ['/usr/bin/fallocate']
+      cmd.extend(['-l', str(size_in_mb)+'M'])
+      cmd.append(img_file)
+      RunCommand(cmd, args.verbose)
+
+      # Format the image to F2FS
+      cmd = ['make_f2fs']
+      cmd.extend(['-g', 'android'])
+      uu = str(uuid.uuid5(uuid.NAMESPACE_URL, 'www.android.com'))
+      cmd.extend(['-U', uu])
+      cmd.extend(['-T', '0'])
+      cmd.append('-r') # sets checkpointing seed to 0 to remove random bits
+      cmd.append(img_file)
+      RunCommand(cmd, args.verbose)
+
+      # Add files to the image
+      cmd = ['sload_f2fs']
+      cmd.extend(['-C', args.canned_fs_config])
+      cmd.extend(['-f', manifests_dir])
+      cmd.extend(['-s', args.file_contexts])
+      cmd.extend(['-T', '0'])
+      cmd.append(img_file)
+      RunCommand(cmd, args.verbose, expected_return_values={0,1})
+
+      cmd = ['sload_f2fs']
+      cmd.extend(['-C', args.canned_fs_config])
+      cmd.extend(['-f', args.input_dir])
+      cmd.extend(['-s', args.file_contexts])
+      cmd.extend(['-T', '0'])
+      cmd.append(img_file)
+      RunCommand(cmd, args.verbose, expected_return_values={0,1})
+
+      # TODO(b/158453869): resize the image file to save space
 
     if args.unsigned_payload_only:
       shutil.copyfile(img_file, args.output)
diff --git a/apexer/runtests.sh b/apexer/runtests.sh
index bc634c0..e2eacc9 100755
--- a/apexer/runtests.sh
+++ b/apexer/runtests.sh
@@ -28,10 +28,12 @@
 export APEXER_TOOL_PATH="${ANDROID_BUILD_TOP}/out/soong/host/linux-x86/bin:${ANDROID_BUILD_TOP}/prebuilts/sdk/tools/linux/bin"
 PATH+=":${ANDROID_BUILD_TOP}/prebuilts/sdk/tools/linux/bin"
 
+for fs_type in ext4 f2fs
+do
 input_dir=$(mktemp -d)
 output_dir=$(mktemp -d)
 
-function finish {
+function cleanup {
   sudo umount /dev/loop10
   sudo losetup --detach /dev/loop10
 
@@ -39,7 +41,7 @@
   rm -rf ${output_dir}
 }
 
-trap finish EXIT
+trap cleanup ERR
 #############################################
 # prepare the inputs
 #############################################
@@ -82,6 +84,7 @@
 ${ANDROID_HOST_OUT}/bin/apexer --verbose --manifest ${manifest_file} \
   --file_contexts ${file_contexts_file} \
   --canned_fs_config ${canned_fs_config_file} \
+  --payload_fs_type ${fs_type} \
   --key ${ANDROID_BUILD_TOP}/system/apex/apexer/testdata/com.android.example.apex.pem \
   --android_jar_path ${ANDROID_BUILD_TOP}/prebuilts/sdk/current/public/android.jar \
   ${input_dir} ${output_file}
@@ -132,4 +135,8 @@
 # check the android manifest
 aapt dump xmltree ${output_file} AndroidManifest.xml
 
-echo Passed
+echo "Passed for ${fs_type}"
+cleanup
+done
+
+echo "Passed for all fs types"
diff --git a/proto/apex_build_info.proto b/proto/apex_build_info.proto
index e25baa2..fd8a349 100644
--- a/proto/apex_build_info.proto
+++ b/proto/apex_build_info.proto
@@ -48,4 +48,7 @@
 
   // Value of --logging_parent passed at build time.
   string logging_parent = 9;
+
+  // Value of --payload_fs_type passed at build time.
+  string payload_fs_type = 10;
 }