Repository: hu-macsy/simexpal

Issue:
Support builds without VCS
Support builds that do not use git. Note that since revisions need some kind of commit identifier, we can only support git-less builds for dev revisions.

Discussion:

Patch:
diff --git a/docs/builds.rst b/docs/builds.rst
index 184c220..695bb4b 100644
--- a/docs/builds.rst
+++ b/docs/builds.rst
@@ -14,12 +14,15 @@ only Git is supported) and build them automatically. To achieve this, we need to
 
 On this page we will explain the ``builds`` key and mainly use the
 `C++ example <https://github.com/hu-macsy/simexpal/tree/master/examples/sorting_cpp>`_ from the
-:ref:`QuickStart` guide in order to do so. For example, we will explain how to specify a Git repository, enable
-the automated build support and the directories involved in the build process. Furthermore we will present
-additional options such as pulling Git submodules and setting additional environment variables for the build process.
+:ref:`QuickStart` guide in order to do so. For example, we will explain how to specify a Git repository or
+a local build that does not come from a VCS, enable the automated build support and the directories involved
+in the build process. Furthermore we will present additional options such as pulling Git submodules and setting
+additional environment variables for the build process.
 
 You can find information regarding the ``revisions`` key on the :ref:`Revisions` page.
 
+.. _SpecifyingGitRepository:
+
 Specifying a Git Repository
 ---------------------------
 
@@ -60,6 +63,19 @@ key to ``true`` in order for simexpal to pull the respective files.
 
 If ``recursive-clone`` is not set, it will default to ``false``.
 
+Specifying Builds Without Version Control
+-----------------------------------------
+
+Specifying builds without version control works similar to :ref:`SpecifyingGitRepository`. The differences are
+
+- we omit the ``git`` key *and*
+- store the local build files in ``./develop/<build_name>@<revision_name>``.
+
+**As of now local builds are only supported for** :ref:`DevRevisions` **as we can not guarantee reproducibility
+without some kind of identifier, e.g, a commit hash.**
+
+All the options below also apply for local builds (we just need to omit the ``git`` key, where applicable).
+
 Automated Builds
 ----------------
 
@@ -234,16 +250,14 @@ current build. In this way we make sure that simexpal builds the required builds
 
    builds:
      - name: build1
-       git: '<link_to_git_repo>'
+       ...
        requires:
          - build2
          - build3
        ...
      - name: build2
-       git: '<link_to_git_repo>'
        ...
      - name: build3
-       git: '<link_to_git_repo>'
        ...
 
 .. _BuildDirectories:
@@ -257,7 +271,7 @@ the following subsections we will cover the directories for :ref:`normal revisio
 
 .. _BuildDirectoriesNormalBuilds:
 
-Build Directories for normal Builds
+Build Directories for Normal Builds
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 A :ref:`normal revision<NormalRevisions>` in simexpal uses the ``/builds`` directory, which contains the four
 subdirectories
@@ -315,7 +329,7 @@ simexpal files have the suffix ``.simexpal``.
    ├── experiments.yml
    └── quicksort.cpp
 
-Build Directories for develop Builds
+Build Directories for Develop Builds
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 A :ref:`develop revision<DevRevisions>` in simexpal uses the ``/dev-builds`` directory, which contains the two
@@ -330,7 +344,8 @@ and the ``/develop`` directory, which contains the
 
 during the build process:
 
-The functions of the respective directories are as :ref:`before<BuildDirectoriesNormalBuilds>`.
+The functions of the respective directories are as :ref:`before<BuildDirectoriesNormalBuilds>` (for local builds
+the clone directory contains the local program files).
 
 Below you can find the shortened directory structure of our
 `C++ example <https://github.com/hu-macsy/simexpal/tree/master/examples/sorting_cpp>`_ example
diff --git a/docs/revisions.rst b/docs/revisions.rst
index fed3a66..c77bb76 100644
--- a/docs/revisions.rst
+++ b/docs/revisions.rst
@@ -58,8 +58,8 @@ Develop Revisions
 Specifying develop revisions works similarly to specifying :ref:`normal revisions<NormalRevisions>`. The differences
 are:
 
-- we add another key ``develop`` and set its value to ``true``
-- we leave the values of the ``build_version`` dict as empty string ``''``
+- we add another key ``develop`` and set its value to ``true`` *and*
+- we leave the values of the ``build_version`` dict as empty string ``''``.
 
 Values specified in the ``build_version`` dict will be ignored for develop revisions and simexpal will clone the
 latest project files.
diff --git a/scripts/simex b/scripts/simex
index 14ef63d..3bda771 100755
--- a/scripts/simex
+++ b/scripts/simex
@@ -212,15 +212,15 @@ def select_phases_from_cli(args):
 
 	if args.recheckout:
 		if not args.f:
-			print("This would delete the local git repository for the build and reclone it. Confirm this action"
-					" by using the '-f' flag.")
+			print("This would delete the local git repository for the build and reclone it (does not apply to"
+					" VCS-less dev-builds). Confirm this action by using the '-f' flag.")
 			return []
 		else:
 			return subsequent_phases(simexpal.build.Phase.CHECKOUT)
 	elif args.checkout:
 		if not args.f:
-			print("This would delete the local git repository for the build and reclone it. Confirm this action"
-					" by using the '-f' flag.")
+			print("This would delete the local git repository for the build and reclone it (does not apply to"
+					" VCS-less dev-builds). Confirm this action by using the '-f' flag.")
 			return []
 		else:
 			return [simexpal.build.Phase.CHECKOUT]
diff --git a/simexpal/base.py b/simexpal/base.py
index 7cd4e12..9e549db 100644
--- a/simexpal/base.py
+++ b/simexpal/base.py
@@ -779,7 +779,7 @@ class BuildInfo:
 
 	@property
 	def git_repo(self):
-		return self._build_yml.get('git', '')
+		return self._build_yml.get('git', None)
 
 	@property
 	def recursive_clone(self):
diff --git a/simexpal/build.py b/simexpal/build.py
index 130a5d7..e06bcb1 100644
--- a/simexpal/build.py
+++ b/simexpal/build.py
@@ -67,11 +67,11 @@ class Phase(IntEnum):
 
 def make_build_in_order(cfg, build, wanted_builds, wanted_phases):
 	if not build.revision.is_dev_build:
-		util.try_mkdir('builds/')
+		util.try_mkdir(cfg.basedir + '/builds/')
 		checkout_dir = build.clone_dir
 	else:
-		util.try_mkdir('develop/')
-		util.try_mkdir('dev-builds/')
+		util.try_mkdir(cfg.basedir + '/develop/')
+		util.try_mkdir(cfg.basedir + '/dev-builds/')
 		checkout_dir = build.source_dir
 
 	def num_allocated_cpus():
@@ -199,81 +199,89 @@ def make_build_in_order(cfg, build, wanted_builds, wanted_phases):
 		subprocess.check_call(args, cwd=workdir, env=environ, shell=shell)
 
 	if want_phase(Phase.CHECKOUT):
-		log_phase('checkout')
+		if build.info.git_repo is not None:
+			log_phase('checkout')
 
-		if not build.revision.is_dev_build:
-
-			def git_ref_changed():
-				local_hash = subprocess.check_output(['git', 'rev-parse', generic_tag], cwd=build.repo_dir)
-				local_hash = local_hash.strip().decode()
+			if not build.revision.is_dev_build:
 
-				if local_hash == git_ref:
-					return False
-				else:
-					remote_refs = subprocess.check_output(['git', 'ls-remote', build.info.git_repo, git_ref]).decode().splitlines()
+				def git_ref_changed():
+					local_hash = subprocess.check_output(['git', 'rev-parse', generic_tag], cwd=build.repo_dir)
+					local_hash = local_hash.strip().decode()
 
-					if len(remote_refs) == 0:
-						raise RuntimeError("Git reference '{}' does not exist". format(git_ref))
-					elif len(remote_refs) > 1:
-						raise RuntimeError("Git reference '{}' is ambiguous". format(git_ref))
+					if local_hash == git_ref:
+						return False
 					else:
-						remote_hash, _ = remote_refs[0].split('\t')
+						remote_refs = subprocess.check_output(['git', 'ls-remote', build.info.git_repo, git_ref]).decode().splitlines()
+
+						if len(remote_refs) == 0:
+							raise RuntimeError("Git reference '{}' does not exist". format(git_ref))
+						elif len(remote_refs) > 1:
+							raise RuntimeError("Git reference '{}' is ambiguous". format(git_ref))
+						else:
+							remote_hash, _ = remote_refs[0].split('\t')
 
-						if local_hash == remote_hash:
-							return False
+							if local_hash == remote_hash:
+								return False
 
-				return True
+					return True
 
-			git_ref = build.revision.version_for_build(build.name)
-			generic_tag = 'refs/tags/simexpal-rev/' + build.revision.name
+				git_ref = build.revision.version_for_build(build.name)
+				generic_tag = 'refs/tags/simexpal-rev/' + build.revision.name
 
-			# Fetch the remote ref to a local tag.
-			fetch_refspec = ['+' + git_ref + ':' + generic_tag]
+				# Fetch the remote ref to a local tag.
+				fetch_refspec = ['+' + git_ref + ':' + generic_tag]
 
-			# TODO: If we *know* that the ref is a tag, we want to do something like the following:
-			#fetch_refspec = ['+refs/tags/' + git_ref + ':' + generic_tag]
+				# TODO: If we *know* that the ref is a tag, we want to do something like the following:
+				#fetch_refspec = ['+refs/tags/' + git_ref + ':' + generic_tag]
 
-			# Create the repository (in an empty state).
-			if not os.access(build.repo_dir, os.F_OK):
-				subprocess.check_call(['git', 'init', '-q', '--bare', build.repo_dir])
+				# Create the repository (in an empty state).
+				if not os.access(build.repo_dir, os.F_OK):
+					subprocess.check_call(['git', 'init', '-q', '--bare', build.repo_dir])
 
-			# Fetch the specified revision if it does not exist already.
-			verify_ref_result = subprocess.call(['git', '--git-dir', build.repo_dir,
-					'rev-parse', '-q', '--verify', generic_tag],
-				stdout=subprocess.DEVNULL)
+				# Fetch the specified revision if it does not exist already.
+				verify_ref_result = subprocess.call(['git', '--git-dir', build.repo_dir,
+						'rev-parse', '-q', '--verify', generic_tag],
+					stdout=subprocess.DEVNULL)
 
-			if verify_ref_result != 0 or git_ref_changed():
-				# As we create generic_tag, we can add --no-tags here.
+				if verify_ref_result != 0 or git_ref_changed():
+					# As we create generic_tag, we can add --no-tags here.
+					subprocess.check_call(['git', '--git-dir', build.repo_dir,
+							'fetch', '--depth=1', '--no-tags',
+							build.info.git_repo] + fetch_refspec)
+
+				# Prune the existing worktree.
+				util.try_rmtree(build.clone_dir)
 				subprocess.check_call(['git', '--git-dir', build.repo_dir,
-						'fetch', '--depth=1', '--no-tags',
-						build.info.git_repo] + fetch_refspec)
-
-			# Prune the existing worktree.
-			util.try_rmtree(build.clone_dir)
-			subprocess.check_call(['git', '--git-dir', build.repo_dir,
-					'worktree', 'prune'])
-
-			# Recreate the worktree and check out the specified revision.
-			subprocess.check_call(['git', '--git-dir', build.repo_dir,
-					'worktree', 'add', '--detach',
-					build.clone_dir,
-					generic_tag])
-		else:
-			# Recreate the source directory
-			util.try_rmtree(build.source_dir)
-			util.try_mkdir(build.source_dir)
+						'worktree', 'prune'])
 
-			# Clone the git repository into the build.source_dir
-			subprocess.check_call(['git', 'clone', build.info.git_repo, build.source_dir])
+				# Recreate the worktree and check out the specified revision.
+				subprocess.check_call(['git', '--git-dir', build.repo_dir,
+						'worktree', 'add', '--detach',
+						build.clone_dir,
+						generic_tag])
+			else:
+				# Recreate the source directory
+				util.try_rmtree(build.source_dir)
+				util.try_mkdir(build.source_dir)
 
-		util.touch(os.path.join(checkout_dir, 'checkedout.simexpal'))
+				# Clone the git repository into the build.source_dir
+				subprocess.check_call(['git', 'clone', build.info.git_repo, build.source_dir])
 
-		if build.info.recursive_clone:
-			# Clone submodules recursively
-			subprocess.check_call(['git', 'submodule',
-					'update', '--init', '--recursive'], cwd=checkout_dir)
+			util.touch(os.path.join(checkout_dir, 'checkedout.simexpal'))
 
-		did_work = True
+			if build.info.recursive_clone:
+				# Clone submodules recursively
+				subprocess.check_call(['git', 'submodule',
+						'update', '--init', '--recursive'], cwd=checkout_dir)
+
+			did_work = True
+		else:
+			print("simexpal: Skipping checkout-phase for VCS-less dev-build {}".format(build.name))
+
+	# Make sure that the build source directory exists for VCS-less dev-builds.
+	if build.info.git_repo is None and build.revision.is_dev_build and not os.path.isdir(build.source_dir):
+		raise RuntimeError(f"The source directory of the build '{build.name}' with revision '{build.revision.name}'"
+							f" does not exist.")
 
 	if want_phase(Phase.REGENERATE):
 		log_phase('regenerate')
diff --git a/simexpal/schemes/experiments.json b/simexpal/schemes/experiments.json
index 7d69bdf..e08f587 100644
--- a/simexpal/schemes/experiments.json
+++ b/simexpal/schemes/experiments.json
@@ -44,7 +44,7 @@
 			"type": "array",
 			"items": {
 				"type": "object",
-				"required": ["name", "git"],
+				"required": ["name"],
 				"properties": {
 					"name": {"type": "string"},
 					"git": {"type":  "string"},


Test patch:
diff --git a/tests/builds/experiments_ymls/vcs_less/develop/vcs-less@main/script.py b/tests/builds/experiments_ymls/vcs_less/develop/vcs-less@main/script.py
new file mode 100644
index 0000000..341e0e9
--- /dev/null
+++ b/tests/builds/experiments_ymls/vcs_less/develop/vcs-less@main/script.py
@@ -0,0 +1,2 @@
+
+print("Hello World!")
diff --git a/tests/builds/experiments_ymls/vcs_less/experiments.yml b/tests/builds/experiments_ymls/vcs_less/experiments.yml
new file mode 100644
index 0000000..2659f1c
--- /dev/null
+++ b/tests/builds/experiments_ymls/vcs_less/experiments.yml
@@ -0,0 +1,25 @@
+# This file contains a VCS-less develop build.
+builds:
+  - name: vcs-less
+    regenerate:
+      - args:
+          - 'touch'
+          - 'regenerated'
+    configure:
+      - args:
+          - 'touch'
+          - 'configured'
+    compile:
+      - args:
+          - 'touch'
+          - 'compiled'
+    install:
+      - args:
+          - 'touch'
+          - 'installed'
+
+revisions:
+  - name: main
+    develop: true
+    build_version:
+      'vcs-less': ''
diff --git a/tests/builds/test_builds.py b/tests/builds/test_builds.py
new file mode 100644
index 0000000..27dc94d
--- /dev/null
+++ b/tests/builds/test_builds.py
@@ -0,0 +1,33 @@
+
+import os
+
+from simexpal import base
+from simexpal import build
+
+file_dir = os.path.abspath(os.path.dirname(__file__))
+
+def test_simex_d_vcs_less():
+    cfg = base.config_for_dir(file_dir + '/experiments_ymls/vcs_less/')
+
+    revision = cfg.get_revision('main')
+    vcs_less_build = cfg.get_build('vcs-less', revision)
+
+    build.make_builds(cfg, revision, [vcs_less_build.info], ['vcs-less'], [build.Phase.INSTALL])
+
+    def check_files_in_dir(files, dir):
+        for file_name in files:
+            assert os.path.isfile(dir + file_name)
+
+    source_dir_files = ['/regenerated',
+                        '/regenerated.simexpal']
+    check_files_in_dir(source_dir_files, vcs_less_build.source_dir)
+
+    compile_dir_files = ['/configured',
+                         '/configured.simexpal',
+                         '/compiled',
+                         '/compiled.simexpal',
+                         '/installed']
+    check_files_in_dir(compile_dir_files, vcs_less_build.compile_dir)
+
+    prefix_dir_files = ['/installed.simexpal']
+    check_files_in_dir(prefix_dir_files, vcs_less_build.prefix_dir)