aboutsummaryrefslogtreecommitdiff
path: root/app/tasks/minetestcheck/tree.py
blob: 57a769f6297e66aa9a0c557f9277b2dcbaa95cb8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
import os, re
from . import MinetestCheckError, ContentType
from .config import parse_conf

basenamePattern = re.compile("^([a-z0-9_]+)$")

def get_base_dir(path):
	if not os.path.isdir(path):
		raise IOError("Expected dir")

	root, subdirs, files = next(os.walk(path))
	if len(subdirs) == 1 and len(files) == 0:
		return get_base_dir(path + "/" + subdirs[0])
	else:
		return path


def detect_type(path):
	if os.path.isfile(path + "/game.conf"):
		return ContentType.GAME
	elif os.path.isfile(path + "/init.lua"):
		return ContentType.MOD
	elif os.path.isfile(path + "/modpack.txt") or \
			os.path.isfile(path + "/modpack.conf"):
		return ContentType.MODPACK
	elif os.path.isdir(path + "/mods"):
		return ContentType.GAME
	elif os.path.isfile(path + "/texture_pack.conf"):
		return ContentType.TXP
	else:
		return ContentType.UNKNOWN


def get_csv_line(line):
	if line is None:
		return []

	return [x.strip() for x in line.split(",") if x.strip() != ""]


class PackageTreeNode:
	def __init__(self, baseDir, relative, author=None, repo=None, name=None):
		self.baseDir  = baseDir
		self.relative = relative
		self.author   = author
		self.name	 = name
		self.repo	 = repo
		self.meta	 = None
		self.children = []

		# Detect type
		self.type = detect_type(baseDir)
		self.read_meta()

		if self.type == ContentType.GAME:
			if not os.path.isdir(baseDir + "/mods"):
				raise MinetestCheckError(("Game at {} does not have a mods/ folder").format(self.relative))
			self.add_children_from_mod_dir("mods")
		elif self.type == ContentType.MOD:
			if self.name and not basenamePattern.match(self.name):
				raise MinetestCheckError(("Invalid base name for mod {} at {}, names must only contain a-z0-9_.") \
					.format(self.name, self.relative))
		elif self.type == ContentType.MODPACK:
			self.add_children_from_mod_dir(None)


	def getMetaFilePath(self):
		filename = None
		if self.type == ContentType.GAME:
			filename = "game.conf"
		elif self.type == ContentType.MOD:
			filename = "mod.conf"
		elif self.type == ContentType.MODPACK:
			filename = "modpack.conf"
		elif self.type == ContentType.TXP:
			filename = "texture_pack.conf"
		else:
			return None

		return self.baseDir + "/" + filename


	def read_meta(self):
		result = {}

		# .conf file
		try:
			with open(self.getMetaFilePath() or "", "r") as myfile:
				conf = parse_conf(myfile.read())
				for key, value in conf.items():
					result[key] = value
		except IOError:
			pass

		# description.txt
		if not "description" in result:
			try:
				with open(self.baseDir + "/description.txt", "r") as myfile:
					result["description"] = myfile.read()
			except IOError:
				pass

		# Read dependencies
		if "depends" in result or "optional_depends" in result:
			result["depends"] = get_csv_line(result.get("depends"))
			result["optional_depends"] = get_csv_line(result.get("optional_depends"))

		elif os.path.isfile(self.baseDir + "/depends.txt"):
			pattern = re.compile("^([a-z0-9_]+)\??$")

			with open(self.baseDir + "/depends.txt", "r") as myfile:
				contents = myfile.read()
				soft = []
				hard = []
				for line in contents.split("\n"):
					line = line.strip()
					if pattern.match(line):
						if line[len(line) - 1] == "?":
							soft.append( line[:-1])
						else:
							hard.append(line)

				result["depends"] = hard
				result["optional_depends"] = soft

		else:
			result["depends"] = []
			result["optional_depends"] = []


		def checkDependencies(deps):
			for dep in result["depends"]:
				if not basenamePattern.match(dep):
					if " " in dep:
						raise MinetestCheckError(("Invalid dependency name '{}' for mod at {}, did you forget a comma?") \
							.format(dep, self.relative))
					else:
						raise MinetestCheckError(("Invalid dependency name '{}' for mod at {}, names must only contain a-z0-9_.") \
							.format(dep, self.relative))


		# Check dependencies
		checkDependencies(result["depends"])
		checkDependencies(result["optional_depends"])

		# Fix games using "name" as "title"
		if self.type == ContentType.GAME:
			result["title"] = result["name"]
			del result["name"]

		# Calculate Title
		if "name" in result and not "title" in result:
			result["title"] = result["name"].replace("_", " ").title()

		# Calculate short description
		if "description" in result:
			desc = result["description"]
			idx = desc.find(".") + 1
			cutIdx = min(len(desc), 200 if idx < 5 else idx)
			result["short_description"] = desc[:cutIdx]

		if "name" in result:
			self.name = result["name"]
			del result["name"]

		self.meta = result

	def add_children_from_mod_dir(self, subdir):
		dir = self.baseDir
		relative = self.relative
		if subdir:
			dir += "/" + subdir
			relative += subdir + "/"

		for entry in next(os.walk(dir))[1]:
			path = os.path.join(dir, entry)
			if not entry.startswith('.') and os.path.isdir(path):
				child = PackageTreeNode(path, relative + entry + "/", name=entry)
				if not child.type.isModLike():
					raise MinetestCheckError(("Expecting mod or modpack, found {} at {} inside {}") \
							.format(child.type.value, child.relative, self.type.value))

				if child.name is None:
					raise MinetestCheckError(("Missing base name for mod at {}").format(self.relative))

				self.children.append(child)

	def getModNames(self):
		return self.fold("name", type=ContentType.MOD)

	# attr: Attribute name
	# key: Key in attribute
	# retval: Accumulator
	# type: Filter to type
	def fold(self, attr, key=None, retval=None, type=None):
		if retval is None:
			retval = set()

		# Iterate through children
		for child in self.children:
			child.fold(attr, key, retval, type)

		# Filter on type
		if type and type != self.type:
			return retval

		# Get attribute
		at = getattr(self, attr)
		if not at:
			return retval

		# Get value
		value = at if key is None else at.get(key)
		if isinstance(value, list):
			retval |= set(value)
		elif value:
			retval.add(value)

		return retval

	def get(self, key):
		return self.meta.get(key)

	def validate(self):
		for child in self.children:
			child.validate()