2023-06-09 17:23:15 +02:00
#!/usr/bin/env python3
# Copyright (C) 2023 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
2023-10-26 20:42:24 +02:00
import asyncio
2023-06-09 17:23:15 +02:00
import collections
import json
2023-07-13 02:55:28 +02:00
import os
2023-10-26 20:42:24 +02:00
import socket
2023-06-09 17:23:15 +02:00
import subprocess
2023-07-13 02:55:28 +02:00
import sys
2023-10-26 20:42:24 +02:00
import textwrap
2023-06-09 17:23:15 +02:00
2023-07-13 02:55:28 +02:00
def get_top ( ) - > str :
path = ' . '
while not os . path . isfile ( os . path . join ( path , ' build/soong/tests/genrule_sandbox_test.py ' ) ) :
if os . path . abspath ( path ) == ' / ' :
sys . exit ( ' Could not find android source tree root. ' )
path = os . path . join ( path , ' .. ' )
return os . path . abspath ( path )
2023-06-09 17:23:15 +02:00
2023-10-26 20:42:24 +02:00
async def _build_with_soong ( out_dir , targets , * , extra_env = { } ) :
env = os . environ | extra_env
# Use nsjail to remap the out_dir to out/, because some genrules write the path to the out
# dir into their artifacts, so if the out directories were different it would cause a diff
# that doesn't really matter.
2023-06-09 17:23:15 +02:00
args = [
2023-10-26 20:42:24 +02:00
' prebuilts/build-tools/linux-x86/bin/nsjail ' ,
' -q ' ,
' --cwd ' ,
os . getcwd ( ) ,
' -e ' ,
' -B ' ,
' / ' ,
' -B ' ,
f ' { os . path . abspath ( out_dir ) } : { os . path . abspath ( " out " ) } ' ,
' --time_limit ' ,
' 0 ' ,
' --skip_setsid ' ,
' --keep_caps ' ,
' --disable_clone_newcgroup ' ,
' --disable_clone_newnet ' ,
' --rlimit_as ' ,
' soft ' ,
' --rlimit_core ' ,
' soft ' ,
' --rlimit_cpu ' ,
' soft ' ,
' --rlimit_fsize ' ,
' soft ' ,
' --rlimit_nofile ' ,
' soft ' ,
' --proc_rw ' ,
' --hostname ' ,
socket . gethostname ( ) ,
' -- ' ,
2023-06-09 17:23:15 +02:00
" build/soong/soong_ui.bash " ,
" --make-mode " ,
" --skip-soong-tests " ,
]
args . extend ( targets )
2023-10-26 20:42:24 +02:00
process = await asyncio . create_subprocess_exec (
* args ,
stdout = asyncio . subprocess . PIPE ,
stderr = asyncio . subprocess . PIPE ,
env = env ,
)
stdout , stderr = await process . communicate ( )
if process . returncode != 0 :
print ( stdout )
print ( stderr )
sys . exit ( process . returncode )
2023-06-09 17:23:15 +02:00
2023-10-26 20:42:24 +02:00
async def _find_outputs_for_modules ( modules ) :
module_path = " out/soong/module-actions.json "
2023-06-09 17:23:15 +02:00
if not os . path . exists ( module_path ) :
2023-10-26 20:42:24 +02:00
await _build_with_soong ( ' out ' , [ " json-module-graph " ] )
2023-06-09 17:23:15 +02:00
2023-07-13 02:55:28 +02:00
with open ( module_path ) as f :
action_graph = json . load ( f )
2023-06-09 17:23:15 +02:00
module_to_outs = collections . defaultdict ( set )
for mod in action_graph :
name = mod [ " Name " ]
if name in modules :
2023-10-26 20:42:24 +02:00
for act in ( mod [ " Module " ] [ " Actions " ] or [ ] ) :
2023-06-15 18:28:26 +02:00
if " }generate " in act [ " Desc " ] :
2023-06-09 17:23:15 +02:00
module_to_outs [ name ] . update ( act [ " Outputs " ] )
return module_to_outs
2023-07-13 02:55:28 +02:00
def _compare_outputs ( module_to_outs , tempdir ) - > dict [ str , list [ str ] ] :
2023-06-09 17:23:15 +02:00
different_modules = collections . defaultdict ( list )
for module , outs in module_to_outs . items ( ) :
for out in outs :
2023-07-13 02:55:28 +02:00
try :
subprocess . check_output ( [ " diff " , os . path . join ( tempdir , out ) , out ] )
except subprocess . CalledProcessError as e :
different_modules [ module ] . append ( e . stdout )
2023-06-09 17:23:15 +02:00
return different_modules
2023-10-26 20:42:24 +02:00
async def main ( ) :
2023-06-09 17:23:15 +02:00
parser = argparse . ArgumentParser ( )
parser . add_argument (
" modules " ,
nargs = " + " ,
help = " modules to compare builds with genrule sandboxing enabled/not " ,
)
2023-10-26 20:42:24 +02:00
parser . add_argument (
" --check-determinism " ,
action = " store_true " ,
help = " Don ' t check for working sandboxing. Instead, run two default builds, and compare their outputs. This is used to check for nondeterminsim, which would also affect the sandboxed test. " ,
)
2023-06-09 17:23:15 +02:00
parser . add_argument (
" --show-diff " ,
" -d " ,
action = " store_true " ,
help = " whether to display differing files " ,
)
2023-06-09 17:29:48 +02:00
parser . add_argument (
" --output-paths-only " ,
" -o " ,
action = " store_true " ,
help = " Whether to only return the output paths per module " ,
)
2023-06-09 17:23:15 +02:00
args = parser . parse_args ( )
2023-07-13 02:55:28 +02:00
os . chdir ( get_top ( ) )
2023-06-09 17:23:15 +02:00
2023-10-26 20:42:24 +02:00
if " TARGET_PRODUCT " not in os . environ :
sys . exit ( " Please run lunch first " )
if os . environ . get ( " OUT_DIR " , " out " ) != " out " :
sys . exit ( f " This script expects OUT_DIR to be ' out ' , got: ' { os . environ . get ( ' OUT_DIR ' ) } ' " )
2023-06-09 17:23:15 +02:00
2023-07-13 02:55:28 +02:00
print ( " finding output files for the modules... " )
2023-10-26 20:42:24 +02:00
module_to_outs = await _find_outputs_for_modules ( set ( args . modules ) )
2023-06-15 18:28:26 +02:00
if not module_to_outs :
2023-07-13 02:55:28 +02:00
sys . exit ( " No outputs found " )
2023-06-15 18:28:26 +02:00
2023-06-09 17:29:48 +02:00
if args . output_paths_only :
for m , o in module_to_outs . items ( ) :
print ( f " { m } outputs: { o } " )
2023-07-13 02:55:28 +02:00
sys . exit ( 0 )
all_outs = list ( set . union ( * module_to_outs . values ( ) ) )
2023-10-26 20:42:24 +02:00
for i , out in enumerate ( all_outs ) :
if not out . startswith ( " out/ " ) :
sys . exit ( " Expected output file to start with out/, found: " + out )
other_out_dir = " out_check_determinism " if args . check_determinism else " out_not_sandboxed "
other_env = { " GENRULE_SANDBOXING " : " false " }
if args . check_determinism :
other_env = { }
# nsjail will complain if the out dir doesn't exist
os . makedirs ( " out " , exist_ok = True )
os . makedirs ( other_out_dir , exist_ok = True )
print ( " building... " )
await asyncio . gather (
_build_with_soong ( " out " , all_outs ) ,
_build_with_soong ( other_out_dir , all_outs , extra_env = other_env )
)
diffs = collections . defaultdict ( dict )
for module , outs in module_to_outs . items ( ) :
for out in outs :
try :
subprocess . check_output ( [ " diff " , os . path . join ( other_out_dir , out . removeprefix ( " out/ " ) ) , out ] )
except subprocess . CalledProcessError as e :
diffs [ module ] [ out ] = e . stdout
if len ( diffs ) == 0 :
print ( " All modules are correct " )
elif args . show_diff :
for m , files in diffs . items ( ) :
print ( f " Module { m } has diffs: " )
for f , d in files . items ( ) :
print ( " " + f + " : " )
print ( textwrap . indent ( d , " " ) )
else :
print ( f " Modules { list ( diffs . keys ( ) ) } have diffs in these files: " )
all_diff_files = [ f for m in diffs . values ( ) for f in m ]
for f in all_diff_files :
print ( f )
2023-07-13 02:55:28 +02:00
2023-06-09 17:23:15 +02:00
if __name__ == " __main__ " :
2023-10-26 20:42:24 +02:00
asyncio . run ( main ( ) )