[make] make sure no command fails

Summary:
Run with `SHELL = bash -e -u -o pipefail` to catch many kinds of failures. We
were silently failing during `make install` because of some missing escaping,
and the failure was hidden because it was happening inside a bash `for` loop.
This fixes the escaping issue and makes sure such issues will result in an
error as of now.

Also removes dangerous `find -exec` instances: `find` will `exit 0` event if
some commands failed.

Fixes #887

Reviewed By: mbouaziz

Differential Revision: D7569054

fbshipit-source-id: 542fe50
master
Jules Villard 7 years ago committed by Facebook Github Bot
parent 594ddab2a5
commit f1bcb91542

@ -285,8 +285,7 @@ ocaml_unit_test: test_build
INFER_ARGS=--results-dir^infer-out-unit-tests $(BUILD_DIR)/test/inferunit.bc) INFER_ARGS=--results-dir^infer-out-unit-tests $(BUILD_DIR)/test/inferunit.bc)
define silence_make define silence_make
($(1) 2> >(grep -v "warning: \(ignoring old\|overriding\) \(commands\|recipe\) for target") \ $(1) 2> >(grep -v "warning: \(ignoring old\|overriding\) \(commands\|recipe\) for target")
; exit $${PIPESTATUS[0]})
endef endef
.PHONY: $(DIRECT_TESTS:%=direct_%_test) .PHONY: $(DIRECT_TESTS:%=direct_%_test)
@ -424,7 +423,7 @@ test-replace: $(BUILD_SYSTEMS_TESTS:%=build_%_replace) $(DIRECT_TESTS:%=direct_%
.PHONY: uninstall .PHONY: uninstall
uninstall: uninstall:
$(REMOVE_DIR) $(DESTDIR)$(libdir)/infer/ $(REMOVE_DIR) $(DESTDIR)$(libdir)/infer/
$(REMOVE) $(DESTDIR)$(bindir)/infer $(REMOVE) $(DESTDIR)$(bindir)/infer*
$(REMOVE) $(INFER_COMMANDS:%=$(DESTDIR)$(bindir)/%) $(REMOVE) $(INFER_COMMANDS:%=$(DESTDIR)$(bindir)/%)
$(REMOVE) $(foreach manual,$(INFER_MANUALS_GZIPPED),\ $(REMOVE) $(foreach manual,$(INFER_MANUALS_GZIPPED),\
$(DESTDIR)$(mandir)/man1/$(notdir $(manual))) $(DESTDIR)$(mandir)/man1/$(notdir $(manual)))
@ -435,112 +434,101 @@ test_clean: $(DIRECT_TESTS:%=direct_%_clean) $(BUILD_SYSTEMS_TESTS:%=build_%_cle
.PHONY: install .PHONY: install
install: infer $(INFER_MANUALS_GZIPPED) install: infer $(INFER_MANUALS_GZIPPED)
# create directory structure # create directory structure
test -d $(DESTDIR)$(bindir) || \ test -d '$(DESTDIR)$(bindir)' || \
$(MKDIR_P) $(DESTDIR)$(bindir) $(MKDIR_P) '$(DESTDIR)$(bindir)'
test -d $(DESTDIR)$(mandir)/man1 || \ test -d '$(DESTDIR)$(mandir)/man1' || \
$(MKDIR_P) $(DESTDIR)$(mandir)/man1 $(MKDIR_P) '$(DESTDIR)$(mandir)/man1'
test -d $(DESTDIR)$(libdir)/infer/ || \ test -d '$(DESTDIR)$(libdir)/infer/' || \
$(MKDIR_P) $(DESTDIR)$(libdir)/infer/ $(MKDIR_P) '$(DESTDIR)$(libdir)/infer/'
ifeq ($(BUILD_C_ANALYZERS),yes) ifeq ($(BUILD_C_ANALYZERS),yes)
test -d $(DESTDIR)$(libdir)/infer/facebook-clang-plugins/libtooling/build/ || \ test -d '$(DESTDIR)$(libdir)/infer/facebook-clang-plugins/libtooling/build/' || \
$(MKDIR_P) $(DESTDIR)$(libdir)/infer/facebook-clang-plugins/libtooling/build/ $(MKDIR_P) '$(DESTDIR)$(libdir)/infer/facebook-clang-plugins/libtooling/build/'
$(QUIET)for i in $$(find facebook-clang-plugins/clang/install -type d); do \ find facebook-clang-plugins/clang/install -type d -print0 | xargs -0 -I \{\} \
test -d $(DESTDIR)$(libdir)/infer/$$i || \ $(SHELL) -c "test -d '$(DESTDIR)$(libdir)'/infer/{} || \
$(MKDIR_P) $(DESTDIR)$(libdir)/infer/$$i; \ $(MKDIR_P) '$(DESTDIR)$(libdir)'/infer/{}"
done test -d '$(DESTDIR)$(libdir)/infer/infer/lib/clang_wrappers/' || \
test -d $(DESTDIR)$(libdir)/infer/infer/lib/clang_wrappers/ || \ $(MKDIR_P) '$(DESTDIR)$(libdir)/infer/infer/lib/clang_wrappers/'
$(MKDIR_P) $(DESTDIR)$(libdir)/infer/infer/lib/clang_wrappers/ find infer/models/cpp/include -type d -print0 | xargs -0 -I \{\} \
$(QUIET)for i in $$(find infer/models/cpp/include/ -type d); do \ $(SHELL) -c "test -d '$(DESTDIR)$(libdir)'/infer/{} || \
test -d $(DESTDIR)$(libdir)/infer/$$i || \ $(MKDIR_P) '$(DESTDIR)$(libdir)'/infer/{}"
$(MKDIR_P) $(DESTDIR)$(libdir)/infer/$$i; \ test -d '$(DESTDIR)$(libdir)/infer/infer/lib/linter_rules/' || \
done $(MKDIR_P) '$(DESTDIR)$(libdir)/infer/infer/lib/linter_rules/'
test -d $(DESTDIR)$(libdir)/infer/infer/lib/linter_rules/ || \ test -d '$(DESTDIR)$(libdir)/infer/infer/etc/' || \
$(MKDIR_P) $(DESTDIR)$(libdir)/infer/infer/lib/linter_rules $(MKDIR_P) '$(DESTDIR)$(libdir)/infer/infer/etc'
test -d $(DESTDIR)$(libdir)/infer/infer/etc/ || \
$(MKDIR_P) $(DESTDIR)$(libdir)/infer/infer/etc
endif endif
ifeq ($(BUILD_JAVA_ANALYZERS),yes) ifeq ($(BUILD_JAVA_ANALYZERS),yes)
test -d $(DESTDIR)$(libdir)/infer/infer/lib/java/ || \ test -d '$(DESTDIR)$(libdir)/infer/infer/lib/java/' || \
$(MKDIR_P) $(DESTDIR)$(libdir)/infer/infer/lib/java/ $(MKDIR_P) '$(DESTDIR)$(libdir)/infer/infer/lib/java/'
endif endif
test -d $(DESTDIR)$(libdir)/infer/infer/annotations/ || \ test -d '$(DESTDIR)$(libdir)/infer/infer/annotations/' || \
$(MKDIR_P) $(DESTDIR)$(libdir)/infer/infer/annotations/ $(MKDIR_P) '$(DESTDIR)$(libdir)/infer/infer/annotations/'
test -d $(DESTDIR)$(libdir)/infer/infer/lib/wrappers/ || \ test -d '$(DESTDIR)$(libdir)/infer/infer/lib/wrappers/' || \
$(MKDIR_P) $(DESTDIR)$(libdir)/infer/infer/lib/wrappers/ $(MKDIR_P) '$(DESTDIR)$(libdir)/infer/infer/lib/wrappers/'
test -d $(DESTDIR)$(libdir)/infer/infer/lib/specs/ || \ test -d '$(DESTDIR)$(libdir)/infer/infer/lib/specs/' || \
$(MKDIR_P) $(DESTDIR)$(libdir)/infer/infer/lib/specs/ $(MKDIR_P) '$(DESTDIR)$(libdir)/infer/infer/lib/specs/'
test -d $(DESTDIR)$(libdir)/infer/infer/lib/python/ || \ test -d '$(DESTDIR)$(libdir)/infer/infer/lib/python/' || \
$(MKDIR_P) $(DESTDIR)$(libdir)/infer/infer/lib/python/ $(MKDIR_P) '$(DESTDIR)$(libdir)/infer/infer/lib/python/'
test -d $(DESTDIR)$(libdir)/infer/infer/lib/python/inferlib/ || \ test -d '$(DESTDIR)$(libdir)/infer/infer/lib/python/inferlib/' || \
$(MKDIR_P) $(DESTDIR)$(libdir)/infer/infer/lib/python/inferlib/ $(MKDIR_P) '$(DESTDIR)$(libdir)/infer/infer/lib/python/inferlib/'
test -d $(DESTDIR)$(libdir)/infer/infer/lib/python/inferlib/capture/ || \ test -d '$(DESTDIR)$(libdir)/infer/infer/lib/python/inferlib/capture/' || \
$(MKDIR_P) $(DESTDIR)$(libdir)/infer/infer/lib/python/inferlib/capture/ $(MKDIR_P) '$(DESTDIR)$(libdir)/infer/infer/lib/python/inferlib/capture/'
test -d $(DESTDIR)$(libdir)/infer/infer/bin/ || \ test -d '$(DESTDIR)$(libdir)/infer/infer/bin/' || \
$(MKDIR_P) $(DESTDIR)$(libdir)/infer/infer/bin/ $(MKDIR_P) '$(DESTDIR)$(libdir)/infer/infer/bin/'
# copy files # copy files
ifeq ($(BUILD_C_ANALYZERS),yes) ifeq ($(BUILD_C_ANALYZERS),yes)
$(INSTALL_DATA) -C facebook-clang-plugins/libtooling/build/FacebookClangPlugin.dylib \ $(INSTALL_DATA) -C 'facebook-clang-plugins/libtooling/build/FacebookClangPlugin.dylib' \
$(DESTDIR)$(libdir)/infer/facebook-clang-plugins/libtooling/build/FacebookClangPlugin.dylib '$(DESTDIR)$(libdir)/infer/facebook-clang-plugins/libtooling/build/FacebookClangPlugin.dylib'
$(QUIET)for i in $$(find facebook-clang-plugins/clang/install -not -type d); do \ find facebook-clang-plugins/clang/install -not -type d -print0 | xargs -0 -I \{\} \
$(INSTALL_PROGRAM) -C $$i $(DESTDIR)$(libdir)/infer/$$i; \ $(INSTALL_PROGRAM) -C \{\} '$(DESTDIR)$(libdir)'/infer/\{\}
done find infer/lib/clang_wrappers/* -print0 | xargs -0 -I \{\} \
$(QUIET)for i in $$(find infer/lib/clang_wrappers/*); do \ $(INSTALL_PROGRAM) -C \{\} '$(DESTDIR)$(libdir)'/infer/\{\}
$(INSTALL_PROGRAM) -C $$i $(DESTDIR)$(libdir)/infer/$$i; \
done
# only for files that point to infer # only for files that point to infer
(cd $(DESTDIR)$(libdir)/infer/infer/lib/wrappers/ && \ (cd '$(DESTDIR)$(libdir)/infer/infer/lib/wrappers/' && \
$(foreach cc,$(shell find $(LIB_DIR)/wrappers -type l), \ $(foreach cc,$(shell find '$(LIB_DIR)/wrappers' -type l), \
[ $(cc) -ef $(INFER_BIN) ] && \ [ $(cc) -ef '$(INFER_BIN)' ] && \
$(REMOVE) $(notdir $(cc)) && \ $(REMOVE) '$(notdir $(cc))' && \
$(LN_S) ../../bin/infer $(notdir $(cc));)) $(LN_S) ../../bin/infer '$(notdir $(cc))';))
$(QUIET)for i in $$(find infer/lib/specs/*); do \ find infer/lib/specs/* -print0 | xargs -0 -I \{\} \
$(INSTALL_DATA) -C $$i $(DESTDIR)$(libdir)/infer/$$i; \ $(INSTALL_DATA) -C \{\} '$(DESTDIR)$(libdir)'/infer/\{\}
done find infer/models/cpp/include -not -type d -print0 | xargs -0 -I \{\} \
$(QUIET)for i in $$(find infer/models/cpp/include/ -not -type d); do \ $(INSTALL_DATA) -C \{\} '$(DESTDIR)$(libdir)'/infer/\{\}
$(INSTALL_DATA) -C $$i $(DESTDIR)$(libdir)/infer/$$i; \ $(INSTALL_DATA) -C 'infer/lib/linter_rules/linters.al' \
done '$(DESTDIR)$(libdir)/infer/infer/lib/linter_rules/linters.al'
$(INSTALL_DATA) -C infer/lib/linter_rules/linters.al \ $(INSTALL_DATA) -C 'infer/etc/clang_ast.dict' \
$(DESTDIR)$(libdir)/infer/infer/lib/linter_rules/linters.al '$(DESTDIR)$(libdir)/infer/infer/etc/clang_ast.dict'
$(INSTALL_DATA) -C infer/etc/clang_ast.dict \
$(DESTDIR)$(libdir)/infer/infer/etc/clang_ast.dict
endif endif
ifeq ($(BUILD_JAVA_ANALYZERS),yes) ifeq ($(BUILD_JAVA_ANALYZERS),yes)
$(INSTALL_DATA) -C infer/annotations/annotations.jar \ $(INSTALL_DATA) -C 'infer/annotations/annotations.jar' \
$(DESTDIR)$(libdir)/infer/infer/annotations/annotations.jar '$(DESTDIR)$(libdir)/infer/infer/annotations/annotations.jar'
$(QUIET)for i in infer/lib/java/*.jar; do \ find infer/lib/java/*.jar -print0 | xargs -0 -I \{\} \
$(INSTALL_DATA) -C $$i $(DESTDIR)$(libdir)/infer/$$i; \ $(INSTALL_DATA) -C \{\} '$(DESTDIR)$(libdir)'/infer/\{\}
done $(INSTALL_PROGRAM) -C '$(LIB_DIR)'/wrappers/javac \
$(INSTALL_PROGRAM) -C $(LIB_DIR)/wrappers/javac \ '$(DESTDIR)$(libdir)'/infer/infer/lib/wrappers/
$(DESTDIR)$(libdir)/infer/infer/lib/wrappers/ endif
endif find infer/lib/python/inferlib/* -type f -print0 | xargs -0 -I \{\} \
$(QUIET)for i in $$(find infer/lib/python/inferlib/* -type f); do \ $(INSTALL_DATA) -C \{\} '$(DESTDIR)$(libdir)'/infer/\{\}
$(INSTALL_DATA) -C $$i $(DESTDIR)$(libdir)/infer/$$i; \
done
$(INSTALL_PROGRAM) -C infer/lib/python/infer.py \ $(INSTALL_PROGRAM) -C infer/lib/python/infer.py \
$(DESTDIR)$(libdir)/infer/infer/lib/python/infer.py '$(DESTDIR)$(libdir)'/infer/infer/lib/python/infer.py
$(INSTALL_PROGRAM) -C infer/lib/python/inferTraceBugs \ $(INSTALL_PROGRAM) -C infer/lib/python/inferTraceBugs \
$(DESTDIR)$(libdir)/infer/infer/lib/python/inferTraceBugs '$(DESTDIR)$(libdir)'/infer/infer/lib/python/inferTraceBugs
$(INSTALL_PROGRAM) -C infer/lib/python/report.py \ $(INSTALL_PROGRAM) -C infer/lib/python/report.py \
$(DESTDIR)$(libdir)/infer/infer/lib/python/report.py '$(DESTDIR)$(libdir)'/infer/infer/lib/python/report.py
$(INSTALL_PROGRAM) -C $(INFER_BIN) $(DESTDIR)$(libdir)/infer/infer/bin/ $(INSTALL_PROGRAM) -C '$(INFER_BIN)' '$(DESTDIR)$(libdir)'/infer/infer/bin/
(cd $(DESTDIR)$(bindir)/ && \ (cd '$(DESTDIR)$(bindir)/' && \
$(REMOVE) infer && \ $(REMOVE) infer && \
$(LN_S) $(libdir_relative_to_bindir)/infer/infer/bin/infer infer) $(LN_S) '$(libdir_relative_to_bindir)'/infer/infer/bin/infer infer)
for alias in $(INFER_COMMANDS); do \ for alias in $(INFER_COMMANDS); do \
(cd $(DESTDIR)$(bindir)/ && \ (cd '$(DESTDIR)$(bindir)'/ && \
$(REMOVE) $$alias && \ $(REMOVE) "$$alias" && \
$(LN_S) infer $$alias); done $(LN_S) infer "$$alias"); done
for alias in $(INFER_COMMANDS); do \ for alias in $(INFER_COMMANDS); do \
(cd $(DESTDIR)$(libdir)/infer/infer/bin && \ (cd '$(DESTDIR)$(libdir)'/infer/infer/bin && \
$(REMOVE) $$alias && \ $(REMOVE) "$$alias" && \
$(LN_S) infer $$alias); done $(LN_S) infer "$$alias"); done
$(QUIET)for i in $(MAN_DIR)/man1/*; do \ find '$(MAN_DIR)'/man1 -print0 | xargs -0 \
$(INSTALL_DATA) -C $$i $(DESTDIR)$(mandir)/man1/$$(basename $$i); \ $(SHELL) -c '$(INSTALL_DATA) -C $$1 "$(DESTDIR)$(mandir)/man1/$$(basename $$1)"'
done
ifeq ($(IS_FACEBOOK_TREE),yes) ifeq ($(IS_FACEBOOK_TREE),yes)
$(QUIET)$(MAKE) -C facebook install $(MAKE) -C facebook install
endif endif
# Nuke objects built from OCaml. Useful when changing the OCaml compiler, for instance. # Nuke objects built from OCaml. Useful when changing the OCaml compiler, for instance.

@ -6,7 +6,7 @@
# of patent rights can be found in the PATENTS file in the same directory. # of patent rights can be found in the PATENTS file in the same directory.
ORIG_SHELL = $(shell echo "$$SHELL") ORIG_SHELL = $(shell echo "$$SHELL")
SHELL = bash SHELL = bash -e -o pipefail -u
ORIG_SHELL_PATH = $(shell printf "%s" "$$PATH") ORIG_SHELL_PATH = $(shell printf "%s" "$$PATH")
@ -153,10 +153,10 @@ else
# Detect if we are already wrapped inside a silent_on_success call and try not to clutter the output # Detect if we are already wrapped inside a silent_on_success call and try not to clutter the output
# too much in that case. # too much in that case.
define silent_on_success define silent_on_success
if [ "$$INSIDE_SILENT_ON_SUCCESS" = 1 ]; then \ if [ -n "$${INSIDE_SILENT_ON_SUCCESS-}" ]; then \
echo "*** inner $(1)"; \ echo '*** inner $(1)'; \
echo "*** inner command: $(2)"; \ echo '*** inner command: $(2)'; \
echo "*** inner CWD: $(CURDIR)"; \ echo '*** inner CWD: $(CURDIR)'; \
($(2)); \ ($(2)); \
exit $$?; \ exit $$?; \
fi; \ fi; \
@ -165,7 +165,7 @@ define silent_on_success
UNIX_START_DATE=$$(date +"%s"); \ UNIX_START_DATE=$$(date +"%s"); \
HUMAN_START_DATE=$$(date +"%H:%M:%S"); \ HUMAN_START_DATE=$$(date +"%H:%M:%S"); \
if [ -z $(SILENT) ]; then \ if [ -z $(SILENT) ]; then \
printf "[%s][%$(MAX_PID_SIZE)s] $(TERM_INFO)%s...$(TERM_RESET)\n" \ printf '[%s][%$(MAX_PID_SIZE)s] $(TERM_INFO)%s...$(TERM_RESET)\n' \
"$$HUMAN_START_DATE" "$$HASH" "$(1)"; \ "$$HUMAN_START_DATE" "$$HASH" "$(1)"; \
fi; \ fi; \
$(MKDIR_P) $(ABSOLUTE_ROOT_DIR)/_build_logs; \ $(MKDIR_P) $(ABSOLUTE_ROOT_DIR)/_build_logs; \
@ -173,9 +173,9 @@ define silent_on_success
2>$(ABSOLUTE_ROOT_DIR)/_build_logs/cmd-$$HASH.err; \ 2>$(ABSOLUTE_ROOT_DIR)/_build_logs/cmd-$$HASH.err; \
ERRCODE=$$?; \ ERRCODE=$$?; \
if [ $$ERRCODE != 0 ]; then \ if [ $$ERRCODE != 0 ]; then \
echo "$(TERM_ERROR)[*ERROR**][$$HASH] *** ERROR $(1)$(TERM_RESET)" >&2; \ echo "$(TERM_ERROR)[*ERROR**][$$HASH] *** ERROR '$(1)'$(TERM_RESET)" >&2; \
echo "$(TERM_ERROR)[*ERROR**][$$HASH] *** command: $(2)$(TERM_RESET)" >&2; \ echo "$(TERM_ERROR)[*ERROR**][$$HASH] *** command: '$(2)'$(TERM_RESET)" >&2; \
echo "$(TERM_ERROR)[*ERROR**][$$HASH] *** CWD: $(CURDIR)$(TERM_RESET)" >&2; \ echo "$(TERM_ERROR)[*ERROR**][$$HASH] *** CWD: '$(CURDIR)'$(TERM_RESET)" >&2; \
echo "$(TERM_ERROR)[*ERROR**][$$HASH] *** stdout:$(TERM_RESET)" >&2; \ echo "$(TERM_ERROR)[*ERROR**][$$HASH] *** stdout:$(TERM_RESET)" >&2; \
sed -e "s/^\(.*\)$$/$(TERM_ERROR)[*ERROR**][$$HASH]$(TERM_RESET) \1/" \ sed -e "s/^\(.*\)$$/$(TERM_ERROR)[*ERROR**][$$HASH]$(TERM_RESET) \1/" \
$(ABSOLUTE_ROOT_DIR)/_build_logs/cmd-$$HASH.out; >&2; \ $(ABSOLUTE_ROOT_DIR)/_build_logs/cmd-$$HASH.out; >&2; \

@ -177,7 +177,6 @@ pre-compiled here:
fi fi
# end if($enable_c_analyzers) # end if($enable_c_analyzers)
# OCaml dependencies # OCaml dependencies
AC_PROG_OCAML AC_PROG_OCAML
AC_ASSERT_PROG([ocamlc], [$OCAMLC]) AC_ASSERT_PROG([ocamlc], [$OCAMLC])

@ -14,10 +14,11 @@ include $(ROOT_DIR)/Makefile.config
TEST_REL_DIR = $(patsubst $(abspath $(TESTS_DIR))/%,%,$(abspath $(CURDIR))) TEST_REL_DIR = $(patsubst $(abspath $(TESTS_DIR))/%,%,$(abspath $(CURDIR)))
define check_no_duplicates define check_no_duplicates
grep "DUPLICATE_SYMBOLS" $(1); test $$? -ne 0 || \ if grep -q "DUPLICATE_SYMBOLS" $(1); then \
(echo '$(TEST_ERROR)Duplicate symbols found in $(CURDIR).' \ printf '$(TERM_ERROR)Duplicate symbols found in $(CURDIR).$(TERM_RESET)\n' >&2; \
'Please make sure all the function names in all the source test files are different.$(TEST_RESET)';\ printf '$(TERM_ERROR)Please make sure all the function names in all the source test files are different.$(TERM_RESET)' >&2; \
exit 1) exit 1; \
fi
endef endef
define check_no_diff define check_no_diff

@ -24,8 +24,8 @@ default: compile
issues.exp.test: $(CLANG_DEPS) $(SOURCES) issues.exp.test: $(CLANG_DEPS) $(SOURCES)
$(QUIET)$(call silent_on_success,Testing Infer fails on issue,\ $(QUIET)$(call silent_on_success,Testing Infer fails on issue,\
($(INFER_BIN) --fail-on-issue -- clang $(CLANG_OPTIONS) $(SOURCES); \ exit_code=0; $(INFER_BIN) --fail-on-issue -- clang $(CLANG_OPTIONS) $(SOURCES) || exit_code=$$?; \
echo "infer exit code: $$?" > $@)) echo "infer exit code: $$exit_code" > $@)
.PHONY: compile .PHONY: compile
compile: $(OBJECTS) compile: $(OBJECTS)

@ -29,10 +29,10 @@ print: capture
.PHONY: test .PHONY: test
test: capture test: capture
$(QUIET)for file in $(SOURCES) ; do \ $(QUIET)error=0; for file in $(SOURCES) ; do \
diff -u $$file.dot $$file.test.dot || error=1 ; \ diff -u "$$file.dot" "$$file.test.dot" || error=1 ; \
done ; \ done ; \
if [ 0$$error -eq 1 ]; then exit 1; fi if [ $$error = 1 ]; then exit 1; fi
.PHONY: replace .PHONY: replace
replace: capture replace: capture

Loading…
Cancel
Save