Merge "Add a test for correctness of C++ compilation."
diff --git a/android/variable.go b/android/variable.go
index 90cb6ff..9478c0c 100644
--- a/android/variable.go
+++ b/android/variable.go
@@ -463,12 +463,13 @@
 	*v = productVariables{
 		BuildNumberFile: stringPtr("build_number.txt"),
 
-		Platform_version_name:             stringPtr("S"),
-		Platform_sdk_version:              intPtr(30),
-		Platform_sdk_codename:             stringPtr("S"),
-		Platform_sdk_final:                boolPtr(false),
-		Platform_version_active_codenames: []string{"S"},
-		Platform_vndk_version:             stringPtr("S"),
+		Platform_version_name:               stringPtr("S"),
+		Platform_base_sdk_extension_version: intPtr(30),
+		Platform_sdk_version:                intPtr(30),
+		Platform_sdk_codename:               stringPtr("S"),
+		Platform_sdk_final:                  boolPtr(false),
+		Platform_version_active_codenames:   []string{"S"},
+		Platform_vndk_version:               stringPtr("S"),
 
 		HostArch:                   stringPtr("x86_64"),
 		HostSecondaryArch:          stringPtr("x86"),
diff --git a/bp2build/symlink_forest.go b/bp2build/symlink_forest.go
index 15a6335..7d48191 100644
--- a/bp2build/symlink_forest.go
+++ b/bp2build/symlink_forest.go
@@ -90,6 +90,20 @@
 	}
 }
 
+func isDir(path string, fi os.FileInfo) bool {
+	if (fi.Mode() & os.ModeSymlink) != os.ModeSymlink {
+		return fi.IsDir()
+	}
+
+	fi2, err := os.Stat(path)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Cannot stat '%s': %s\n", path, err)
+		os.Exit(1)
+	}
+
+	return fi2.IsDir()
+}
+
 // Recursively plants a symlink forest at forestDir. The symlink tree will
 // contain every file in buildFilesDir and srcDir excluding the files in
 // exclude. Collects every directory encountered during the traversal of srcDir
@@ -145,8 +159,18 @@
 			continue
 		}
 
+		sDir := false
+		bDir := false
+		if sExists {
+			sDir = isDir(shared.JoinPath(topdir, srcChild), srcChildEntry)
+		}
+
+		if bExists {
+			bDir = isDir(shared.JoinPath(topdir, buildFilesChild), buildFilesChildEntry)
+		}
+
 		if !sExists {
-			if buildFilesChildEntry.IsDir() && excludeChild != nil {
+			if bDir && excludeChild != nil {
 				// Not in the source tree, but we have to exclude something from under
 				// this subtree, so descend
 				plantSymlinkForestRecursive(topdir, forestChild, buildFilesChild, srcChild, excludeChild, acc, okay)
@@ -155,7 +179,7 @@
 				symlinkIntoForest(topdir, forestChild, buildFilesChild)
 			}
 		} else if !bExists {
-			if srcChildEntry.IsDir() && excludeChild != nil {
+			if sDir && excludeChild != nil {
 				// Not in the build file tree, but we have to exclude something from
 				// under this subtree, so descend
 				plantSymlinkForestRecursive(topdir, forestChild, buildFilesChild, srcChild, excludeChild, acc, okay)
@@ -163,10 +187,10 @@
 				// Not in the build file tree, symlink source tree, carry on
 				symlinkIntoForest(topdir, forestChild, srcChild)
 			}
-		} else if srcChildEntry.IsDir() && buildFilesChildEntry.IsDir() {
+		} else if sDir && bDir {
 			// Both are directories. Descend.
 			plantSymlinkForestRecursive(topdir, forestChild, buildFilesChild, srcChild, excludeChild, acc, okay)
-		} else if !srcChildEntry.IsDir() && !buildFilesChildEntry.IsDir() {
+		} else if !sDir && !bDir {
 			// Neither is a directory. Prioritize BUILD files generated by bp2build
 			// over any BUILD file imported into external/.
 			fmt.Fprintf(os.Stderr, "Both '%s' and '%s' exist, symlinking the former to '%s'\n",
diff --git a/finder/finder.go b/finder/finder.go
index b4834b1..c5196c8 100644
--- a/finder/finder.go
+++ b/finder/finder.go
@@ -94,6 +94,10 @@
 	// RootDirs are the root directories used to initiate the search
 	RootDirs []string
 
+	// Whether symlinks are followed. If set, symlinks back to their own parent
+	// directory don't work.
+	FollowSymlinks bool
+
 	// ExcludeDirs are directory names that if encountered are removed from the search
 	ExcludeDirs []string
 
@@ -1415,9 +1419,14 @@
 				// If stat fails this is probably a broken or dangling symlink, treat it as a file.
 				subfiles = append(subfiles, child.Name())
 			} else if childStat.IsDir() {
-				// Skip symlink dirs.
-				// We don't have to support symlink dirs because
-				// that would cause duplicates.
+				// Skip symlink dirs if not requested otherwise. Android has a number
+				// of symlinks creating infinite source trees which would otherwise get
+				// us in an infinite loop.
+				// TODO(b/197349722): Revisit this once symlink loops are banned in the
+				// source tree.
+				if f.cacheMetadata.Config.FollowSymlinks {
+					subdirs = append(subdirs, child.Name())
+				}
 			} else {
 				// We do have to support symlink files because the link name might be
 				// different than the target name
diff --git a/finder/finder_test.go b/finder/finder_test.go
index 788dbdd..8f73719 100644
--- a/finder/finder_test.go
+++ b/finder/finder_test.go
@@ -90,6 +90,7 @@
 		CacheParams{
 			"/cwd",
 			[]string{root},
+			false,
 			nil,
 			nil,
 			[]string{"findme.txt", "skipme.txt"},
@@ -121,6 +122,7 @@
 		CacheParams{
 			"/cwd",
 			[]string{root},
+			false,
 			nil,
 			nil,
 			[]string{"findme.txt", "skipme.txt"},
diff --git a/tests/bp2build_bazel_test.sh b/tests/bp2build_bazel_test.sh
index 4f37c2b..74e49aa 100755
--- a/tests/bp2build_bazel_test.sh
+++ b/tests/bp2build_bazel_test.sh
@@ -115,3 +115,57 @@
 }
 
 test_bp2build_generates_all_buildfiles
+
+function test_cc_correctness {
+  setup
+  create_mock_bazel
+
+  mkdir -p a
+  cat > a/Android.bp <<EOF
+cc_object {
+  name: "qq",
+  srcs: ["qq.cc"],
+  bazel_module: {
+    bp2build_available: true,
+  },
+  stl: "none",
+  system_shared_libs: [],
+}
+EOF
+
+  cat > a/qq.cc <<EOF
+#include "qq.h"
+int qq() {
+  return QQ;
+}
+EOF
+
+  cat > a/qq.h <<EOF
+#define QQ 1
+EOF
+
+  run_soong bp2build
+
+  run_bazel build --package_path=out/soong/workspace //a:qq
+  local output_mtime1=$(stat -c "%y" bazel-bin/a/_objs/qq/qq.o)
+
+  run_bazel build --package_path=out/soong/workspace //a:qq
+  local output_mtime2=$(stat -c "%y" bazel-bin/a/_objs/qq/qq.o)
+
+  if [[ "$output_mtime1" != "$output_mtime2" ]]; then
+    fail "output changed on null build"
+  fi
+
+  cat > a/qq.h <<EOF
+#define QQ 2
+EOF
+
+  run_bazel build --package_path=out/soong/workspace //a:qq
+  local output_mtime3=$(stat -c "%y" bazel-bin/a/_objs/qq/qq.o)
+
+  if [[ "$output_mtime1" == "$output_mtime3" ]]; then
+    fail "output not changed when included header changed"
+  fi
+}
+
+test_cc_correctness
diff --git a/tests/lib.sh b/tests/lib.sh
index 1bb2df9..7fd970a 100644
--- a/tests/lib.sh
+++ b/tests/lib.sh
@@ -85,6 +85,7 @@
   copy_directory build/soong
   copy_directory build/make/tools/rbcrun
 
+  symlink_directory prebuilts/sdk
   symlink_directory prebuilts/go
   symlink_directory prebuilts/build-tools
   symlink_directory prebuilts/clang/host
@@ -115,8 +116,10 @@
   copy_directory build/bazel
 
   symlink_directory prebuilts/bazel
+  symlink_directory prebuilts/clang
   symlink_directory prebuilts/jdk
   symlink_directory external/bazel-skylib
+  symlink_directory external/bazelbuild-rules_android
 
   symlink_file WORKSPACE
   symlink_file BUILD
@@ -136,4 +139,5 @@
 
 
 export ALLOW_MISSING_DEPENDENCIES=true
+export ALLOW_BP_UNDER_SYMLINKS=true
 warmup_mock_top
diff --git a/ui/build/finder.go b/ui/build/finder.go
index 262de3d..4d6ad42 100644
--- a/ui/build/finder.go
+++ b/ui/build/finder.go
@@ -64,6 +64,7 @@
 	cacheParams := finder.CacheParams{
 		WorkingDirectory: dir,
 		RootDirs:         []string{"."},
+		FollowSymlinks:   config.environ.IsEnvTrue("ALLOW_BP_UNDER_SYMLINKS"),
 		ExcludeDirs:      []string{".git", ".repo"},
 		PruneFiles:       pruneFiles,
 		IncludeFiles: []string{